AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/10/08 12:29 flow patch-triage
9m Args:
null

Results:
{
  "EnableConfigs": [
    "KAPI_SPEC",
    "KAPI_RUNTIME_CHECKS",
    "KAPI_SPEC_DEBUGFS",
    "DEBUG_FS"
  ],
  "FocusSymbols": [
    "kapi_validate_syscall_params",
    "kapi_validate_syscall_return",
    "kapi_validate_param_with_context",
    "kapi_validate_return_value",
    "kapi_export_json",
    "kapi_register_spec",
    "kapi_unregister_spec",
    "kapi_spec_show",
    "kapi_spec_json_show",
    "kapi_list_show"
  ],
  "KMSANReasoning": "The patch series introduces the Kernel API Specification (KAPI) framework, which provides machine-readable API specifications and optional runtime validation of syscall parameters (via SYSCALL_DEFINEx wrappers), debugfs introspection, tracepoints, and userspace tooling.\n\nEvaluating KMSAN vs. KASAN applicability:\n- KMSAN detects reads of uninitialized memory and info-leaks to userspace. This patch does not introduce any uninitialized memory risks:\n  1. All API specifications compiled into the kernel are static const objects placed in .rodata, where uninitialized fields are guaranteed to be zero-initialized by the C compiler.\n  2. Dynamic registrations use kzalloc_obj, and KUnit test specs use kunit_kzalloc / memset(..., 0, ...).\n  3. No kernel structures or uninitialized paddings are copied to userspace via copy_to_user, put_user, netlink, ioctl, or socket options. The debugfs interface outputs text and JSON formatted via seq_file (scnprintf), and the tracepoint helper explicitly memsets __entry-\u003eargs.\n  4. The runtime validation hooks only perform sanity checks on syscall arguments (checking ranges, masks, and user pointer accessibility via access_ok/strnlen_user) and early-return -EINVAL on violation, without altering buffer lengths or leaving kernel data uninitialized.\n- Potential risks in this patch (e.g. out-of-bounds reads during .kapi_specs section traversal, format string truncation/buffer bounds, or deadlocks on api_spec_mutex) are already detected by standard tools like KASAN and LOCKDEP.\n\nTherefore, a dedicated KMSAN fuzzing session is not justified.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch series introduces the Kernel API Specification Framework (KAPI). It modifies x86 and generic syscall wrappers (__SYSCALL_DEFINEx) to intercept syscall execution, validating syscall parameters against annotations before running the handler and validating return values afterward (initially annotating sys_open, sys_close, sys_read, sys_write, and sys_madvise). It also adds a debugfs interface under /sys/kernel/debug/kapi/ to expose human-readable and JSON-serialized specifications. This modifies reachable core syscall dispatch and VFS/mm execution paths, making it worth fuzzing. kapi_get_spec is intentionally omitted from FocusSymbols to avoid diluting coverage filters across all syscalls.",
  "WorthFuzzing": true
}

1/1 2026/10/08 12:29 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 866c60e83179de8f813261f2eb9fcbe27010e491\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Thu Oct 8 12:29:08 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/.gitignore b/.gitignore\nindex 9875120ea7bde..37e6cd1d83fc9 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -12,6 +12,7 @@\n #\n .*\n *.a\n+*.apispec.h\n *.asn1.[ch]\n *.bc\n *.bin\ndiff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst\nindex 59cbb77b33ff4..8d3768645d96c 100644\n--- a/Documentation/dev-tools/index.rst\n+++ b/Documentation/dev-tools/index.rst\n@@ -36,6 +36,7 @@ Documentation/process/debugging/index.rst\n    kunit/index\n    ktap\n    checkuapi\n+   kernel-api-spec\n    gpio-sloppy-logic-analyzer\n    autofdo\n    propeller\ndiff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/dev-tools/kernel-api-spec.rst\nnew file mode 100644\nindex 0000000000000..818c9b966a0c0\n--- /dev/null\n+++ b/Documentation/dev-tools/kernel-api-spec.rst\n@@ -0,0 +1,648 @@\n+.. SPDX-License-Identifier: GPL-2.0\n+\n+======================================\n+Kernel API Specification Framework\n+======================================\n+\n+:Author: Sasha Levin \u003csashal@kernel.org\u003e\n+\n+.. contents:: Table of Contents\n+   :depth: 3\n+   :local:\n+\n+Introduction\n+============\n+\n+The Kernel API Specification Framework (KAPI) describes kernel APIs in a\n+machine-readable form. The descriptions are written as kerneldoc annotations\n+next to the implementation and compiled into the kernel. They can be used to\n+check system call arguments at runtime and can be read back through debugfs or\n+the ``kapi`` tool.\n+\n+Purpose and Goals\n+-----------------\n+\n+The framework aims to:\n+\n+1. **Improve API Documentation**: Provide structured, inline documentation that\n+   lives alongside the code and is maintained as part of the development process.\n+\n+2. **Enable Runtime Validation**: Optionally validate API usage at runtime to catch\n+   common programming errors during development and testing.\n+\n+3. **Support Tooling**: Export API specifications in machine-readable formats for\n+   use by static analyzers, documentation generators, and development tools. See\n+   `The kapi Tool`_.\n+\n+4. **Formalize Contracts**: Explicitly document API contracts including parameter\n+   constraints, execution contexts, locking requirements, and side effects.\n+\n+Architecture Overview\n+=====================\n+\n+Components\n+----------\n+\n+The framework consists of several key components:\n+\n+1. **Core Framework** (``kernel/api/kernel_api_spec.c``)\n+\n+   - API specification registration and storage\n+   - Runtime validation engine\n+   - Specification lookup and querying\n+\n+2. **DebugFS Interface** (``kernel/api/kapi_debugfs.c``)\n+\n+   - Runtime introspection via ``/sys/kernel/debug/kapi/``\n+   - Per-API detailed specification output\n+   - List of all registered API specifications\n+\n+3. **kapi Tool** (``tools/kapi/``)\n+\n+   - Userspace utility for extracting specifications\n+   - Multiple input sources (source, binary, debugfs)\n+   - Multiple output formats (plain, JSON, RST)\n+   - Testing and validation utilities\n+\n+Data Model\n+----------\n+\n+The framework uses a hierarchical data model::\n+\n+    kernel_api_spec\n+    ├── Basic Information\n+    │   ├── name (API function name)\n+    │   ├── version (specification version)\n+    │   └── description (human-readable description)\n+    │\n+    ├── Parameters (up to 16)\n+    │   └── kapi_param_spec\n+    │       ├── name\n+    │       ├── type (int, pointer, fd, path, etc.)\n+    │       ├── flags (in, out, inout, optional, etc.)\n+    │       ├── constraints (range, mask, enum values)\n+    │       └── description\n+    │\n+    ├── Return Value\n+    │   └── kapi_return_spec\n+    │       ├── type\n+    │       ├── success conditions\n+    │       └── validation rules\n+    │\n+    ├── Error Conditions (up to 32)\n+    │   └── kapi_error_spec\n+    │       ├── error code\n+    │       ├── name\n+    │       ├── condition\n+    │       └── description\n+    │\n+    ├── Execution Context\n+    │   ├── allowed contexts (process, interrupt, etc.)\n+    │   ├── locking requirements\n+    │   └── preemption/interrupt state\n+    │\n+    └── Side Effects\n+        ├── memory allocation\n+        ├── state changes\n+        └── signal handling\n+\n+Usage Guide\n+===========\n+\n+Basic API Specification\n+-----------------------\n+\n+API specifications are written as KAPI-annotated kerneldoc comments directly in\n+the source file, immediately preceding the function implementation. With\n+``CONFIG_KAPI_SPEC`` enabled, Kbuild runs ``kernel-doc -apispec`` on each\n+built-in C file that has a ``contexts:`` (or ``context-flags:``) line plus at\n+least one of ``api-type:``, ``param:``, ``error:``, ``capability:``,\n+``signal:``, ``lock:``, ``state-trans:``, ``constraint:``, ``side-effect:`` or\n+``long-desc:``, and compiles the generated header into that file. The ``kapi``\n+tool reads the same annotations, or the specifications in a built kernel, for\n+use outside the kernel build.\n+\n+The following is an excerpt of the ``sys_read`` specification in\n+``fs/read_write.c``, trimmed for length:\n+\n+.. code-block:: c\n+\n+    /**\n+     * sys_read - Read data from a file descriptor\n+     * @fd: File descriptor to read from\n+     * @buf: User-space buffer to read data into\n+     * @count: Maximum number of bytes to read\n+     *\n+     * long-desc: Attempts to read up to count bytes from file descriptor fd into\n+     *   the buffer starting at buf. ...\n+     *\n+     * contexts: process, sleepable\n+     *\n+     * param: fd\n+     *   type: fd, input\n+     *   constraint-type: range(0, INT_MAX)\n+     *\n+     * param: buf\n+     *   type: user_ptr, output\n+     *   constraint-type: buffer(2)\n+     *\n+     * param: count\n+     *   type: uint, input\n+     *\n+     * return:\n+     *   type: int\n+     *   check-type: range\n+     *   success: \u003e= 0\n+     *   desc: On success, returns the number of bytes read (non-negative). ...\n+     *\n+     * error: EBADF, Bad file descriptor\n+     *   desc: fd is not a valid file descriptor, or fd was not opened for\n+     *     reading. ...\n+     */\n+    SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)\n+\n+DSL reference:\n+\n+* ``contexts:`` — comma-separated list of call contexts.  Accepted tokens:\n+  ``process``, ``softirq``, ``hardirq``, ``nmi``, ``atomic``, ``sleepable``,\n+  ``preempt_disabled``, ``irq_disabled``.  ``context-flags:`` with\n+  ``|``-joined ``KAPI_CTX_*`` constants is equivalent.\n+* ``type:`` — parameter type plus direction/qualifier flags on a single\n+  line.  Type aliases (case-insensitive): ``int``, ``uint``, ``ptr``,\n+  ``fd``, ``path``, ``user_ptr`` (or ``uptr``), ``struct``, ``union``,\n+  ``enum``, ``func_ptr``, ``array``, ``custom``.  Flag aliases:\n+  ``input``, ``output``, ``inout``, ``user``, ``optional``, ``const``,\n+  ``volatile``, ``dma``, ``aligned``.\n+* ``constraint-type:`` — a ``KAPI_CONSTRAINT_*`` enum token or a\n+  function-call expression.  ``range(lo, hi)``, ``mask(expr)``,\n+  ``enum(v1, v2, …)``, ``buffer(size_param_idx)``, ``alignment(N)``,\n+  ``user_string``, ``user_path``, ``user_ptr``, ``power_of_two``,\n+  ``page_aligned``, ``nonzero``.  ``user_string`` takes its length limits\n+  from ``range:``.  The function-call\n+  form populates the matching aux fields\n+  (``range:`` / ``valid-mask:`` / ``size-param:``).\n+* ``arch-mask:`` — extends a ``mask(...)`` constraint with bits that\n+  are valid only on a named architecture.  The form is\n+  ``arch-mask: \u003carch\u003e = \u003cbits-expr\u003e`` and may appear multiple times.\n+  The named arch must be one of the known short names\n+  (``alpha``, ``arc``, ``arm``, ``arm64``, ``csky``, ``hexagon``,\n+  ``loongarch``, ``m68k``, ``microblaze``, ``mips``, ``nios2``,\n+  ``openrisc``, ``parisc``, ``powerpc``, ``riscv``, ``s390``, ``sh``,\n+  ``sparc``, ``um``, ``x86``, ``xtensa``); the generator translates it\n+  to the matching ``CONFIG_*`` symbol and emits the bits inside an\n+  ``#ifdef`` so a single generated apispec.h compiles on every\n+  architecture and folds in the arch-specific bits at compile time.\n+  Use this for PROT/MAP bits whose UAPI symbols are defined only on\n+  the matching arch.\n+* ``lock: … type:`` accepts ``mutex``, ``spinlock``, ``rwlock``,\n+  ``seqlock``, ``rcu``, ``semaphore``, ``custom`` or ``KAPI_LOCK_*``.\n+* ``signal: … direction:`` accepts ``receive``, ``send``, ``handle``,\n+  ``block``, ``ignore`` (bitmask, joinable with ``|`` or ``,``).\n+* ``signal: … action:`` accepts ``default``, ``terminate``, ``coredump``,\n+  ``stop``, ``continue``, ``custom``, ``return``, ``restart``,\n+  ``queue``, ``discard``, ``transform``.\n+* ``signal: … timing:`` accepts ``before``, ``during``, ``after``.\n+* ``capability: … type:`` accepts ``bypass_check``, ``increase_limit``,\n+  ``override_restriction``, ``grant_permission``, ``modify_behavior``,\n+  ``access_resource``, ``perform_operation``.\n+* ``side-effect:`` accepts the snake_case effect names\n+  (``alloc_memory``, ``free_memory``, ``modify_state``, ``signal_send``,\n+  ``file_position``, ``lock_acquire``, ``lock_release``,\n+  ``resource_create``, ``resource_destroy``, ``schedule``, ``hardware``,\n+  ``network``, ``filesystem``, ``process_state``, ``irreversible``)\n+  joined with ``|`` — for example ``side-effect: resource_create | alloc_memory``.\n+* ``return: … type:`` reuses the ``type:`` aliases above.\n+* ``return: … check-type:`` accepts ``exact``, ``range``,\n+  ``fd``, ``no_return``.  ``success:``\n+  gives the success value for ``exact`` (an integer, optionally\n+  written ``= N``) and the lower bound for ``range`` (``\u003e= N``); the\n+  other check types do not use it.  The ``type:`` of a return block\n+  is also kept as written (for example ``int``) in the\n+  human-readable ``type_name`` of ``struct kapi_return_spec``.\n+* ``error:`` takes a ``NAME, one-line summary`` header followed\n+  by optional indented ``desc:`` / ``condition:`` subfields.\n+* ``lock:`` and ``signal:`` take an indented ``desc:`` subfield\n+  for the long-form description; ``signal:`` also accepts\n+  ``number:`` (the signal constant, for example ``SIGPIPE``),\n+  ``errno:``, ``priority:``, ``restartable:``, ``interruptible:``,\n+  and ``queue:`` subfields.\n+* ``state-trans:`` takes ``from:``, ``to:``, ``object:``,\n+  optional ``condition:``, and ``desc:`` subfields.  The condition is\n+  stored in its own field, apart from the description.\n+* ``long-desc:`` is a free-form multi-line prose block that\n+  populates ``long_description`` in the spec.  ``notes:`` is a\n+  free-form block of the same kind.  In both, a blank line starts a\n+  new paragraph, a line starting with ``- `` stays on its own line so\n+  bullet lists survive, and any other wrapped line is joined to the\n+  previous one with a space.  ``examples:`` keeps one example per\n+  line, preserving the relative indentation of nested code.  The\n+  line breaks are stored as ``\\n`` in the generated strings, which\n+  have no length limit.\n+* A subfield line inside a block starts with one of the subfield\n+  names of that block followed by ``:``.  Every other line, even one\n+  with a colon in the middle of a sentence, continues the previous\n+  subfield.\n+* ``param-count:`` is optional; the parser counts ``param:`` blocks and\n+  warns when an explicit count disagrees.\n+\n+System Call Specification\n+-------------------------\n+\n+System calls are documented inline in the implementation file (e.g., ``fs/open.c``)\n+using KAPI-annotated kerneldoc comments. When ``CONFIG_KAPI_RUNTIME_CHECKS`` is\n+enabled, the ``SYSCALL_DEFINEx`` macros automatically look up the specification\n+and validate parameters before and after the syscall executes.\n+\n+Runtime Validation\n+==================\n+\n+Enabling Validation\n+-------------------\n+\n+Runtime validation is controlled by kernel configuration:\n+\n+1. Enable ``CONFIG_KAPI_SPEC`` to build the framework\n+2. Enable ``CONFIG_KAPI_RUNTIME_CHECKS`` for runtime validation\n+3. Optionally enable ``CONFIG_KAPI_SPEC_DEBUGFS`` for debugfs interface\n+\n+Validation Behavior\n+-------------------\n+\n+When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, every system call that has a\n+specification is validated in its ``SYSCALL_DEFINEx()`` wrapper: the arguments\n+are checked against the parameter constraints before the handler runs, and the\n+return value is checked against the return specification afterwards. Violations\n+are reported via ``pr_warn_ratelimited`` to avoid flooding the kernel log. On\n+the return side only a successful ``fd`` return that is not a valid file\n+descriptor is reported. Any other value that does not satisfy the success check\n+is treated as an error and accepted, and error codes that the specification\n+does not list are only logged at debug level.\n+The execution context recorded in a specification is not checked at runtime.\n+The option is available on x86 and on architectures that use the generic\n+``__SYSCALL_DEFINEx()``.\n+\n+.. warning::\n+\n+   Userspace errno is affected when this option is on. For syscalls that\n+   violate their parameter specification, KAPI short-circuits the call and\n+   returns ``-EINVAL`` from the validator **before** the real handler runs.\n+   That errno can differ from what the real handler would have produced for\n+   the same condition (for example, ``-ENOMEM`` from an allocation path or\n+   ``-EFAULT`` from a deeper copy-in). ``CONFIG_KAPI_RUNTIME_CHECKS`` is a\n+   debug-only option; do not enable it on production kernels or in\n+   userspace-visible test environments where error-code fidelity matters.\n+\n+Custom Validators\n+-----------------\n+\n+``KAPI_CONSTRAINT_CUSTOM`` calls the ``validate`` function of the parameter\n+specification. Kerneldoc annotations cannot set it, so it is only available to a\n+``struct kapi_param_spec`` that is filled in by hand:\n+\n+.. code-block:: c\n+\n+    static bool validate_buffer_size(s64 value)\n+    {\n+        size_t size = (size_t)value;\n+\n+        return size \u003e 0 \u0026\u0026 size \u003c= MAX_BUFFER_SIZE;\n+    }\n+\n+    /* In the parameter definition: */\n+    .constraint_type = KAPI_CONSTRAINT_CUSTOM,\n+    .validate = validate_buffer_size,\n+\n+Tracepoints\n+-----------\n+\n+When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, the syscall validation path emits\n+two ftrace tracepoints (in the ``kapi`` trace system) for every syscall that has a\n+specification:\n+\n+- ``kapi_syscall_enter`` -- fired before parameter validation, recording the spec\n+  name, the raw syscall argument values, and -- when the spec provides parameter\n+  metadata -- a rendered ``name=value`` list: signed integer and file descriptor\n+  values are shown in decimal, all other values in hex, and an unnamed parameter\n+  as ``arg``.\n+- ``kapi_syscall_exit`` -- fired after the handler returns, or in place of the\n+  handler when parameter validation rejects the call (the handler is skipped and\n+  ``-EINVAL`` is returned). Records the spec name, the return value, and\n+  ``spec_match``: 0 when the parameters were rejected or a successful ``fd`` return\n+  was not a valid file descriptor, and 1 otherwise. Error codes missing from the\n+  spec do not clear it.\n+\n+Unlike the ``pr_warn_ratelimited`` violation reports, the tracepoints capture every\n+spec'd call rather than only violations, and can be filtered with the usual ftrace\n+facilities. They require ``CONFIG_TRACEPOINTS`` and stay dormant\n+until enabled::\n+\n+    # echo 1 \u003e /sys/kernel/tracing/events/kapi/enable\n+    # cat /sys/kernel/tracing/trace\n+     ...  kapi_syscall_enter: sys_read(fd=3, buf=0x7ffd46780b58, count=0x340)\n+     ...  kapi_syscall_exit: sys_read = 832 spec_match=1\n+     ...  kapi_syscall_enter: sys_open(filename=0x480300, flags=268435456, mode=0x0)\n+     ...  kapi_syscall_exit: sys_open = -22 spec_match=0\n+\n+DebugFS Interface\n+=================\n+\n+The debugfs interface provides runtime access to API specifications:\n+\n+Directory Structure\n+-------------------\n+\n+::\n+\n+    /sys/kernel/debug/kapi/\n+    ├── list                     # Overview of all registered API specs\n+    ├── specs/                   # Per-API specification files\n+    │   ├── sys_open             # Human-readable spec for sys_open\n+    │   ├── sys_close            # Human-readable spec for sys_close\n+    │   ├── sys_read             # Human-readable spec for sys_read\n+    │   ├── sys_write            # Human-readable spec for sys_write\n+    │   └── sys_madvise          # Human-readable spec for sys_madvise\n+    └── specs-json/              # Machine-readable counterpart of specs/\n+        ├── sys_open             # JSON spec for sys_open\n+        └── ...\n+\n+Usage Examples\n+--------------\n+\n+List all available API specifications::\n+\n+    $ cat /sys/kernel/debug/kapi/list\n+    Available Kernel API Specifications\n+    ===================================\n+\n+    sys_open - Open or create a file\n+    sys_close - Close a file descriptor\n+    sys_read - Read data from a file descriptor\n+    sys_write - Write data to a file descriptor\n+    sys_madvise - Give advice about use of memory\n+\n+    Total: 5 specifications\n+\n+Query specific API::\n+\n+    $ cat /sys/kernel/debug/kapi/specs/sys_open\n+    Kernel API Specification\n+    ========================\n+\n+    Name: sys_open\n+    Version: 1\n+    Description: Open or create a file\n+    ...\n+\n+The ``specs-json/`` files carry the complete specification: for each\n+parameter the type class, flags and the constraint (type, range, valid\n+mask, enumerated values, alignment, size and the index of the parameter\n+holding a buffer's size), the return check, errors, locks, signals,\n+signal masks, side effects, state transitions, capabilities, additional\n+constraints and structure specifications. Masks and flag words are\n+hex strings, enumerations are lower-case tokens such as\n+``\"constraint_type\": \"buffer\"``, and ``size_param_idx`` is a 0-based\n+index into ``parameters`` (``null`` when unused). The ``kapi`` tool\n+reads these files with ``--debugfs`` and reports the same data as\n+``--vmlinux``.\n+\n+Performance Considerations\n+==========================\n+\n+Memory Overhead\n+---------------\n+\n+Each compiled spec is 26400 bytes (``readelf -sW vmlinux | grep\n+__kapi_spec_``), dominated by the fixed-bound arrays\n+``struct_specs[8]`` (11584 bytes), ``signal_masks[32]`` (4864),\n+``signals[32]`` (3328) and ``params[16]`` (1920). With\n+the five syscall specs in this series, ``.kapi_specs`` and the backing\n+``.rodata`` objects occupy ~132 KB. Building with ``CONFIG_KAPI_SPEC=n``\n+emits no code or data from the framework.\n+\n+Runtime Overhead\n+----------------\n+\n+When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, each validated\n+call pays for a parameter walk plus the per-constraint check\n+(range/mask/enum/align/user-ptr/user-path/user-string).\n+The cost depends on the parameter count and the constraints involved;\n+profile before enabling on workloads where syscall latency matters.\n+``CONFIG_KAPI_RUNTIME_CHECKS=n`` compiles the validators away\n+entirely.\n+\n+The kapi Tool\n+=============\n+\n+Overview\n+--------\n+\n+The ``kapi`` tool is a userspace utility that extracts and displays kernel API\n+specifications from multiple sources. It provides a unified interface to access\n+API documentation whether from compiled kernels, source code, or runtime systems.\n+\n+Installation\n+------------\n+\n+Build the tool from the kernel source tree::\n+\n+    $ cd tools/kapi\n+    $ cargo build --release\n+\n+    # Optional: Install system-wide\n+    $ cargo install --path .\n+\n+The tool requires Rust and Cargo to build. The binary will be available at\n+``tools/kapi/target/release/kapi``.\n+\n+Command-Line Usage\n+------------------\n+\n+Basic syntax::\n+\n+    kapi [OPTIONS] [API_NAME]\n+\n+Options:\n+\n+- ``--vmlinux \u003cPATH\u003e``: Extract from compiled kernel binary\n+- ``--source \u003cPATH\u003e``: Extract from kernel source code\n+- ``--debugfs \u003cPATH\u003e``: Extract from debugfs (default: /sys/kernel/debug)\n+- ``-f, --format \u003cFORMAT\u003e``: Output format (plain, json, rst)\n+- ``-h, --help``: Display help information\n+- ``-V, --version``: Display version information\n+\n+Input Modes\n+-----------\n+\n+**1. Source Code Mode**\n+\n+Extract specifications directly from kernel source::\n+\n+    # Scan entire kernel source tree\n+    $ kapi --source /path/to/linux\n+\n+    # Extract from specific file\n+    $ kapi --source fs/open.c\n+\n+    # Get details for specific API\n+    $ kapi --source /path/to/linux sys_close\n+\n+**2. Vmlinux Mode**\n+\n+Extract from compiled kernel with debug symbols::\n+\n+    # List all APIs in vmlinux\n+    $ kapi --vmlinux ./vmlinux\n+\n+    # Get specific syscall details\n+    $ kapi --vmlinux ./vmlinux sys_read\n+\n+**3. Debugfs Mode**\n+\n+Extract from running kernel via debugfs::\n+\n+    # Use default debugfs path\n+    $ kapi\n+\n+    # Use custom debugfs mount\n+    $ kapi --debugfs /mnt/debugfs\n+\n+    # Get specific API from running kernel\n+    $ kapi sys_write\n+\n+Output Formats\n+--------------\n+\n+The samples below are shortened; ``...`` marks omitted output.\n+\n+**Plain Text Format** (default)::\n+\n+    $ kapi --source . sys_read\n+\n+    Detailed information for sys_read:\n+    ==================================\n+    Description: Read data from a file descriptor\n+\n+    Detailed Description:\n+      Attempts to read up to count bytes from file descriptor fd into the buffer starting at buf. ...\n+\n+    Execution Context:\n+      - KAPI_CTX_PROCESS\n+      - KAPI_CTX_SLEEPABLE\n+\n+    Parameters (3):\n+      [0] fd (unsigned int fd)\n+          File descriptor to read from\n+          Flags: IN\n+          ...\n+\n+**JSON Format**::\n+\n+    $ kapi --source . --format json sys_read\n+    {\n+      \"api_details\": {\n+        \"name\": \"sys_read\",\n+        \"description\": \"Read data from a file descriptor\",\n+        \"long_description\": \"Attempts to read up to count bytes from file descriptor fd into the buffer starting at buf. ...\",\n+        \"context_flags\": [\n+          \"KAPI_CTX_PROCESS\",\n+          \"KAPI_CTX_SLEEPABLE\"\n+        ],\n+        ...\n+      }\n+    }\n+\n+**ReStructuredText Format**::\n+\n+    $ kapi --source . --format rst sys_read\n+\n+    sys_read\n+    ========\n+\n+    **Read data from a file descriptor**\n+\n+    Attempts to read up to count bytes from file descriptor fd into the buffer starting at buf. ...\n+\n+Usage Examples\n+--------------\n+\n+**Generate complete API documentation**::\n+\n+    # Export all kernel APIs to JSON\n+    $ kapi --source /path/to/linux --format json \u003e kernel-apis.json\n+\n+    # Generate RST documentation for all syscalls\n+    $ kapi --vmlinux ./vmlinux --format rst \u003e syscalls.rst\n+\n+    # List APIs from specific subsystem\n+    $ kapi --source fs/\n+\n+**Integration with other tools**::\n+\n+    # List the names of all APIs\n+    $ kapi --format json | jq -r '.apis[].name'\n+\n+    # Generate markdown documentation\n+    $ kapi --format rst sys_madvise | pandoc -f rst -t markdown\n+\n+**Debugging and analysis**::\n+\n+    # Check if specific API exists\n+    $ kapi --source . my_custom_api || echo \"API not found\"\n+\n+Implementation Details\n+----------------------\n+\n+The tool extracts API specifications from three sources:\n+\n+1. **Source Code**: Parses KAPI-annotated kerneldoc comments in C files, using\n+   the same selection rule as Kbuild; regular expressions only locate the\n+   ``SYSCALL_DEFINEx()`` or function that follows each comment\n+2. **Vmlinux**: Reads the ``.kapi_specs`` ELF section from compiled kernels\n+3. **Debugfs**: Reads from ``/sys/kernel/debug/kapi/`` filesystem interface\n+\n+The tool supports all KAPI specification types:\n+\n+- System calls (kerneldoc annotations)\n+- Kernel functions (kerneldoc annotations with KAPI tags)\n+\n+Troubleshooting\n+===============\n+\n+Common Issues\n+-------------\n+\n+**Specification Not Found**\n+\n+A syscall that is missing from ``/sys/kernel/debug/kapi/list`` has no\n+specification. Ensure the KAPI-annotated kerneldoc comment is in the same\n+translation unit as the function implementation, is named ``sys_\u003cname\u003e`` for\n+``SYSCALL_DEFINEx(\u003cname\u003e, ...)``, and has a ``contexts:`` line plus one more\n+KAPI section as described above.\n+\n+**Validation Failures**::\n+\n+    kapi: Parameter fd: invalid file descriptor -1\n+\n+    Solution: Check parameter constraints or adjust specification if\n+    the constraint is incorrect.\n+\n+Debug Options\n+-------------\n+\n+Violations are logged with ``pr_warn_ratelimited()``. Error codes that a\n+specification does not list are logged with ``pr_debug()``; with\n+``CONFIG_DYNAMIC_DEBUG`` they can be enabled with::\n+\n+    echo 'file kernel_api_spec.c +p' \u003e /sys/kernel/debug/dynamic_debug/control\n+\n+Contributing\n+============\n+\n+Submitting Specifications\n+-------------------------\n+\n+1. Add specifications to the same file as the API implementation\n+2. Follow existing patterns and naming conventions\n+3. Test with CONFIG_KAPI_RUNTIME_CHECKS enabled\n+4. Verify debugfs output is correct\n+5. Run scripts/checkpatch.pl on your changes\ndiff --git a/MAINTAINERS b/MAINTAINERS\nindex 65e8a4b5c90b1..20020705921a6 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -14118,6 +14118,19 @@ W:\thttps://linuxtv.org\n T:\tgit git://linuxtv.org/media.git\n F:\tdrivers/media/radio/radio-keene*\n \n+KERNEL API SPECIFICATION FRAMEWORK (KAPI)\n+M:\tSasha Levin \u003csashal@kernel.org\u003e\n+L:\tlinux-api@vger.kernel.org\n+S:\tMaintained\n+F:\tDocumentation/dev-tools/kernel-api-spec.rst\n+F:\tinclude/linux/kapi_syscall.h\n+F:\tinclude/linux/kernel_api_spec.h\n+F:\tinclude/trace/events/kapi.h\n+F:\tkernel/api/\n+F:\ttools/kapi/\n+F:\ttools/lib/python/kdoc/kdoc_apispec.py\n+F:\ttools/testing/selftests/kapi/\n+\n KERNEL AUTOMOUNTER\n M:\tIan Kent \u003craven@themaw.net\u003e\n L:\tautofs@vger.kernel.org\ndiff --git a/Makefile b/Makefile\nindex 7c855e6fe5448..fd7917f78fa45 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2251,6 +2251,7 @@ clean: $(clean-dirs)\n \t\t-o -name '*.ll' \\\n \t\t-o -name '*.gcno' \\\n \t\t-o -name '*.long-type-*.txt' \\\n+\t\t-o -name '*.apispec.h' \\\n \t\t\\) -type f -print \\\n \t\t-o -name '.tmp_*' -print \\\n \t\t| xargs rm -rf\ndiff --git a/arch/x86/include/asm/syscall_wrapper.h b/arch/x86/include/asm/syscall_wrapper.h\nindex 7e88705e907f4..54b70c2cac8ac 100644\n--- a/arch/x86/include/asm/syscall_wrapper.h\n+++ b/arch/x86/include/asm/syscall_wrapper.h\n@@ -223,11 +223,12 @@ extern long __ia32_sys_ni_syscall(const struct pt_regs *regs);\n #define __SYSCALL_DEFINEx(x, name, ...)\t\t\t\t\t\\\n \tstatic long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));\t\\\n \tstatic inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\\\n+\t__KAPI_SYSCALL_DEFINEx(x, name, __VA_ARGS__)\t\t\t\\\n \t__X64_SYS_STUBx(x, name, __VA_ARGS__)\t\t\t\t\\\n \t__IA32_SYS_STUBx(x, name, __VA_ARGS__)\t\t\t\t\\\n \tstatic long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))\t\\\n \t{\t\t\t\t\t\t\t\t\\\n-\t\tlong ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\\\n+\t\tlong ret = __KAPI_DO_SYS(name)(__MAP(x,__SC_CAST,__VA_ARGS__));\\\n \t\t__MAP(x,__SC_TEST,__VA_ARGS__);\t\t\t\t\\\n \t\t__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));\t\\\n \t\treturn ret;\t\t\t\t\t\t\\\ndiff --git a/fs/open.c b/fs/open.c\nindex 6b1c14e684a93..9f08c438f162a 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -1424,6 +1424,330 @@ int do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)\n }\n \n \n+/**\n+ * sys_open - Open or create a file\n+ * @filename: Pathname of the file to open or create\n+ * @flags: File access mode and behavior flags (O_RDONLY, O_WRONLY, O_RDWR, etc.)\n+ * @mode: File permission bits for newly created files (only with O_CREAT/O_TMPFILE)\n+ *\n+ * long-desc: Opens the file named by filename, relative to the current\n+ *   working directory if the path is relative. With O_CREAT, the file is\n+ *   created if it does not exist. With O_TMPFILE, filename must name an\n+ *   existing directory, in which an unnamed file is created. A new file gets\n+ *   mode \u0026 ~umask as its permission bits.\n+ *\n+ *   The low two bits of flags (O_ACCMODE) select the access mode: O_RDONLY,\n+ *   O_WRONLY or O_RDWR. File creation and file status flags are ORed in.\n+ *\n+ *   File creation flags: O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC, O_DIRECTORY,\n+ *   O_NOFOLLOW, O_CLOEXEC, O_TMPFILE, O_EMPTYPATH. O_EMPTYPATH permits an\n+ *   empty filename, which for open() refers to the current working directory.\n+ *\n+ *   File status flags: O_APPEND, FASYNC, O_DIRECT, O_DSYNC, O_LARGEFILE,\n+ *   O_NOATIME, O_NONBLOCK (O_NDELAY), O_PATH, O_SYNC. These become part of the\n+ *   file's open file description and can be retrieved with fcntl(F_GETFL). Only\n+ *   O_APPEND, O_NONBLOCK, O_DIRECT, O_NOATIME and FASYNC can be changed later\n+ *   with fcntl(F_SETFL).\n+ *\n+ *   On success the lowest-numbered file descriptor not currently open in the\n+ *   process is returned.\n+ *\n+ *   On 64-bit systems, O_LARGEFILE is automatically added to the flags. On 32-bit\n+ *   systems, files larger than 2GB require O_LARGEFILE to be explicitly set.\n+ *\n+ *   open() is equivalent to openat(AT_FDCWD, filename, flags, mode).\n+ *\n+ * contexts: process, sleepable\n+ *\n+ * param: filename\n+ *   type: path, input\n+ *   constraint-type: user_path\n+ *   cdesc: Must be a valid null-terminated path string in user memory.\n+ *     Maximum path length is PATH_MAX (4096 bytes) including null terminator.\n+ *     For relative paths, resolution starts from current working directory.\n+ *     The path is followed (symlinks resolved) unless O_NOFOLLOW is specified.\n+ *\n+ * param: flags\n+ *   type: int, input\n+ *   constraint-type: mask(O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_EXCL | O_NOCTTY |\n+ *                         O_TRUNC | O_APPEND | O_NONBLOCK | O_NDELAY | O_DSYNC | O_SYNC |\n+ *                         FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW |\n+ *                         O_NOATIME | O_CLOEXEC | O_PATH | O_TMPFILE | O_EMPTYPATH)\n+ *   cdesc: Should be one of O_RDONLY (0), O_WRONLY (1), or O_RDWR (2) as the\n+ *     access mode. Additional flags may be ORed. O_CREAT combined with\n+ *     O_DIRECTORY or O_TMPFILE, O_TMPFILE without O_DIRECTORY, and O_TMPFILE\n+ *     with read-only mode return EINVAL. With O_PATH, open() silently drops\n+ *     every other flag except O_DIRECTORY, O_NOFOLLOW, O_CLOEXEC and\n+ *     O_EMPTYPATH (only openat2() rejects them with EINVAL). Unknown flags are\n+ *     silently ignored for backward compatibility (unlike openat2 which\n+ *     rejects them).\n+ *\n+ * param: mode\n+ *   type: uint, input\n+ *   cdesc: Only meaningful when O_CREAT or O_TMPFILE is specified in\n+ *     flags. Specifies the file mode bits (permissions and setuid/setgid/sticky\n+ *     bits) for a newly created file. The effective mode is (mode \u0026 ~umask).\n+ *     When O_CREAT/O_TMPFILE is not set, mode is ignored. Mode values exceeding\n+ *     S_IALLUGO (07777) are masked off.\n+ *\n+ * return:\n+ *   type: int\n+ *   check-type: fd\n+ *   success: \u003e= 0\n+ *   desc: On success, returns a new file descriptor (non-negative integer).\n+ *     The returned file descriptor is the lowest-numbered descriptor not\n+ *     currently open for the process. On error, returns a negative error code.\n+ *\n+ * error: EACCES, Permission denied\n+ *   desc: The requested access to the file is not allowed, or search permission\n+ *     is denied for one of the directories in the path prefix of pathname, or\n+ *     the file did not exist yet and write access to the parent directory is\n+ *     not allowed, or O_TRUNC is specified but write permission is denied, or\n+ *     pathname is a device special file on a nodev mount, or O_CREAT on an\n+ *     existing FIFO or regular file in a sticky directory is refused by\n+ *     protected_fifos or protected_regular, or a security module denies the\n+ *     open.\n+ *\n+ * error: EAGAIN, Resource temporarily unavailable\n+ *   desc: O_NONBLOCK was specified and a conflicting lease is held on the file,\n+ *     so the open would have to wait for the lease to break. break_lease()\n+ *     returns -EWOULDBLOCK, which has the same value as EAGAIN.\n+ *\n+ * error: EBUSY, Device or resource busy\n+ *   desc: O_EXCL was specified in flags and pathname refers to a block device\n+ *     that is in use by the system (e.g., it is mounted).\n+ *\n+ * error: EDQUOT, Disk quota exceeded\n+ *   desc: O_CREAT is specified and the file does not exist, and the user's quota\n+ *     of disk blocks or inodes on the filesystem has been exhausted.\n+ *\n+ * error: EEXIST, File exists\n+ *   desc: O_CREAT and O_EXCL were specified in flags, but pathname already exists.\n+ *     This error is atomic with respect to file creation - it prevents race\n+ *     conditions (TOCTOU) when creating files.\n+ *\n+ * error: EFAULT, Bad address\n+ *   desc: pathname points outside the process's accessible address space.\n+ *\n+ * error: EINTR, Interrupted system call\n+ *   desc: A signal arrived while the open was blocked waiting for the partner\n+ *     of a FIFO open (fifo_open), waiting for a conflicting lease to break\n+ *     (__break_lease), or inside a driver's open method. The kernel-internal\n+ *     -ERESTARTSYS is reported as EINTR unless the handler uses SA_RESTART.\n+ *\n+ * error: EINVAL, Invalid argument\n+ *   desc: Returned for several conditions: (1) Invalid O_* flag combinations\n+ *     (O_CREAT with O_DIRECTORY, O_CREAT with O_TMPFILE, O_TMPFILE without\n+ *     O_DIRECTORY, O_TMPFILE with read-only access). (2) O_DIRECT requested\n+ *     but the filesystem does not support it.\n+ *\n+ * error: EISDIR, Is a directory\n+ *   desc: pathname refers to a directory and the access requested involved\n+ *     writing (O_WRONLY, O_RDWR, or O_TRUNC). Also returned when O_CREAT is\n+ *     specified and pathname names an existing directory or ends in a slash.\n+ *\n+ * error: ELOOP, Too many symbolic links\n+ *   desc: Too many symbolic links were encountered in resolving pathname, or\n+ *     O_NOFOLLOW was specified but pathname refers to a symbolic link. With\n+ *     O_PATH and O_NOFOLLOW the symbolic link itself is opened instead.\n+ *\n+ * error: EMFILE, Too many open files\n+ *   desc: The per-process limit on the number of open file descriptors has been\n+ *     reached. This limit is RLIMIT_NOFILE (default typically 1024, max set by\n+ *     /proc/sys/fs/nr_open).\n+ *\n+ * error: ENAMETOOLONG, File name too long\n+ *   desc: pathname was too long, exceeding PATH_MAX (4096) bytes, or a single\n+ *     path component exceeded NAME_MAX (usually 255) bytes.\n+ *\n+ * error: ENFILE, Too many open files in system\n+ *   desc: The system-wide limit on the total number of open files has been\n+ *     reached (/proc/sys/fs/file-max). Processes with CAP_SYS_ADMIN can exceed\n+ *     this limit.\n+ *\n+ * error: ENODEV, No such device\n+ *   desc: The filesystem or driver open method failed with ENODEV, or the\n+ *     file's inode has no file operations assigned. A device special file with\n+ *     no registered device fails with ENXIO instead.\n+ *\n+ * error: ENOENT, No such file or directory\n+ *   desc: A directory component in pathname does not exist or is a dangling\n+ *     symbolic link, or O_CREAT is not set and the named file does not exist,\n+ *     or pathname is an empty string and O_EMPTYPATH is not specified.\n+ *\n+ * error: ENOMEM, Out of memory\n+ *   desc: The kernel could not allocate sufficient memory for the file structure,\n+ *     path lookup structures, or the filename buffer.\n+ *\n+ * error: ENOSPC, No space left on device\n+ *   desc: O_CREAT was specified and the file does not exist, and the directory\n+ *     or filesystem containing the file has no room for a new file entry.\n+ *\n+ * error: ENOTDIR, Not a directory\n+ *   desc: A component used as a directory in pathname is not actually a directory,\n+ *     or O_DIRECTORY was specified and pathname was not a directory.\n+ *\n+ * error: ENXIO, No such device or address\n+ *   desc: O_NONBLOCK | O_WRONLY is set and the named file is a FIFO and no\n+ *     process has the FIFO open for reading. Also returned when opening a device\n+ *     special file whose device does not exist (chrdev_open, blkdev_open), or\n+ *     when opening a socket inode.\n+ *\n+ * error: EOPNOTSUPP, Operation not supported\n+ *   desc: The filesystem containing pathname does not support O_TMPFILE.\n+ *\n+ * error: EOVERFLOW, Value too large for defined data type\n+ *   desc: pathname refers to a regular file that is too large to be opened.\n+ *     This occurs on 32-bit systems without O_LARGEFILE when the file size\n+ *     exceeds 2GB (2^31 - 1 bytes).\n+ *\n+ * error: EPERM, Operation not permitted\n+ *   desc: O_NOATIME flag was specified but the effective UID of the caller did\n+ *     not match the owner of the file and the caller is not privileged, or the\n+ *     file is append-only and O_TRUNC was specified or write mode without\n+ *     O_APPEND, or the file is immutable, or a seal prevents the operation.\n+ *\n+ * error: EROFS, Read-only file system\n+ *   desc: pathname refers to a file on a read-only filesystem and write access\n+ *     was requested.\n+ *\n+ * error: ETXTBSY, Text file busy\n+ *   desc: Write access or O_TRUNC was requested for an executable image that\n+ *     is currently being executed, or O_TRUNC was requested on an active swap\n+ *     file. A swap file can otherwise be opened for writing.\n+ *\n+ * lock: files-\u003efile_lock\n+ *   type: spinlock\n+ *   acquired: true\n+ *   released: true\n+ *   desc: Acquired when allocating a file descriptor slot. Held briefly during\n+ *     fd allocation via alloc_fd() and released before the syscall returns.\n+ *\n+ * lock: inode-\u003ei_rwsem (parent directory)\n+ *   type: semaphore\n+ *   acquired: true\n+ *   released: true\n+ *   desc: Conditional, taken only when the final component is not resolved by\n+ *     the lockless dcache lookup. lookup_open() takes it exclusively with\n+ *     inode_lock() when O_CREAT is set and shared with inode_lock_shared()\n+ *     otherwise. Slow-path lookup of path components takes it shared. Released\n+ *     when the lookup returns. The open path has no killable variant.\n+ *\n+ * lock: RCU read-side\n+ *   type: rcu\n+ *   acquired: true\n+ *   released: true\n+ *   desc: Path lookup uses RCU mode initially for performance. If RCU lookup\n+ *     fails (returns -ECHILD), falls back to reference-based lookup.\n+ *\n+ * signal: Any signal\n+ *   direction: receive\n+ *   action: return\n+ *   condition: When blocked in an interruptible wait\n+ *   desc: The syscall may be interrupted while waiting for the partner of a\n+ *     FIFO open (fifo_open), for a conflicting lease to break (__break_lease),\n+ *     or inside a driver's open method. The wait returns -ERESTARTSYS, which\n+ *     is restarted after the handler with SA_RESTART and reported as EINTR\n+ *     otherwise.\n+ *   errno: -EINTR\n+ *   timing: during\n+ *   restartable: yes\n+ *\n+ * side-effect: resource_create | alloc_memory\n+ *   target: file descriptor, file structure, dentry cache\n+ *   desc: Allocates a new file descriptor in the process's fd table. Allocates\n+ *     a struct file from the filp slab cache. May allocate dentries and inodes\n+ *     during path lookup. System-wide file count (nr_files) is incremented.\n+ *   reversible: yes\n+ *\n+ * side-effect: filesystem\n+ *   target: filesystem, inode\n+ *   condition: When O_CREAT is specified and file doesn't exist\n+ *   desc: Creates a new file on the filesystem. Creates new inode, allocates\n+ *     data blocks as needed, and creates directory entry. Updates parent\n+ *     directory mtime and ctime.\n+ *   reversible: no\n+ *\n+ * side-effect: filesystem\n+ *   target: file content\n+ *   condition: When O_TRUNC is specified for existing file\n+ *   desc: Truncates the file to zero length, releasing data blocks. Updates\n+ *     file mtime and ctime. May trigger notifications to lease holders.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: inode timestamps\n+ *   condition: When a symlink is followed, or O_TRUNC or O_CREAT takes effect\n+ *   desc: Opening does not update the atime of the opened file. Reads update it\n+ *     later, and O_NOATIME only affects those reads. Following a symlink in\n+ *     the path may update the symlink's atime, subject to the mount atime\n+ *     options. O_TRUNC and file creation update mtime and ctime.\n+ *\n+ * capability: CAP_DAC_OVERRIDE\n+ *   type: bypass_check\n+ *   allows: Bypass file read, write, and execute permission checks\n+ *   without: Standard DAC (discretionary access control) checks are applied\n+ *   condition: Checked when file permission would otherwise deny access\n+ *\n+ * capability: CAP_DAC_READ_SEARCH\n+ *   type: bypass_check\n+ *   allows: Bypass read permission on files and search permission on directories\n+ *   without: Must have read permission on file or search permission on directory\n+ *   condition: Checked during path traversal and file open\n+ *\n+ * capability: CAP_FOWNER\n+ *   type: bypass_check\n+ *   allows: Use O_NOATIME on files not owned by caller\n+ *   without: O_NOATIME returns EPERM if caller is not file owner\n+ *   condition: Checked when O_NOATIME is specified and caller is not owner\n+ *\n+ * capability: CAP_SYS_ADMIN\n+ *   type: increase_limit\n+ *   allows: Exceed the system-wide file limit (file-max)\n+ *   without: Returns ENFILE when system limit is reached\n+ *   condition: Checked in alloc_empty_file() when nr_files \u003e= max_files\n+ *\n+ * constraint: RLIMIT_NOFILE (per-process fd limit)\n+ *   desc: The returned file descriptor must be less than the process's\n+ *     RLIMIT_NOFILE limit. Default is typically 1024, maximum is controlled\n+ *     by /proc/sys/fs/nr_open (default 1048576). Exceeding returns EMFILE.\n+ *   expr: fd \u003c rlimit(RLIMIT_NOFILE)\n+ *\n+ * constraint: file-max (system-wide limit)\n+ *   desc: System-wide limit on open files in /proc/sys/fs/file-max. Processes\n+ *     without CAP_SYS_ADMIN receive ENFILE when this limit is reached. The\n+ *     limit is computed based on system memory at boot time.\n+ *   expr: nr_files \u003c files_stat.max_files || capable(CAP_SYS_ADMIN)\n+ *\n+ * constraint: PATH_MAX\n+ *   desc: Maximum length of pathname including null terminator is PATH_MAX\n+ *     (4096 bytes). Individual path components must not exceed NAME_MAX (255).\n+ *\n+ * examples: fd = open(\"/etc/passwd\", O_RDONLY);  // Read existing file\n+ *   fd = open(\"/tmp/newfile\", O_WRONLY | O_CREAT | O_TRUNC, 0644);  // Create/truncate\n+ *   fd = open(\"/tmp/lockfile\", O_WRONLY | O_CREAT | O_EXCL, 0600);  // Exclusive create\n+ *   fd = open(\"/dev/null\", O_RDWR);  // Open device\n+ *   fd = open(\"/tmp\", O_RDONLY | O_DIRECTORY);  // Open directory\n+ *   fd = open(\"/tmp\", O_TMPFILE | O_RDWR, 0600);  // Anonymous temp file\n+ *\n+ * notes: O_RDONLY is defined as 0, so (flags \u0026 O_RDONLY) always evaluates to zero.\n+ *   Test access mode using (flags \u0026 O_ACCMODE) == O_RDONLY.\n+ *\n+ *   When O_CREAT is specified without O_EXCL, there is a race condition between\n+ *   testing for file existence and creating it. Use O_CREAT | O_EXCL for atomic\n+ *   exclusive file creation.\n+ *\n+ *   O_CLOEXEC should be used in multithreaded programs to prevent file descriptor\n+ *   leaks to child processes between fork() and execve().\n+ *\n+ *   O_DIRECT has alignment requirements that vary by filesystem. Use statx()\n+ *   with STATX_DIOALIGN (Linux 6.1+) to query requirements. Unaligned I/O may\n+ *   fail with EINVAL or fall back to buffered I/O.\n+ *\n+ *   O_PATH opens a file descriptor that can be used only for certain operations\n+ *   (fstat, dup, fcntl, close, fchdir on directories, as dirfd for *at() calls).\n+ *   I/O operations will fail with EBADF.\n+ */\n SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)\n {\n \tif (force_o_largefile())\n@@ -1537,6 +1861,239 @@ int filp_close(struct file *filp, fl_owner_t id)\n }\n EXPORT_SYMBOL(filp_close);\n \n+/**\n+ * sys_close - Close a file descriptor\n+ * @fd: The file descriptor to close\n+ *\n+ * long-desc: Terminates access to an open file descriptor, releasing the file\n+ *   descriptor for reuse by subsequent open(), dup(), or similar syscalls.\n+ *\n+ *   Traditional POSIX advisory record locks held by the process on the\n+ *   associated file are released when any of its fds for that inode is\n+ *   closed, not only the last one. OFD locks and flock locks are associated\n+ *   with the open file description and are only released when the last\n+ *   reference to that open file description is dropped.\n+ *\n+ *   Closing a file descriptor drops one reference to its open file\n+ *   description. Other descriptors (dup(), fork(), SCM_RIGHTS messages in\n+ *   flight) and operations still running on the file, such as a concurrent\n+ *   read(), also hold references. Only when the last reference is dropped are\n+ *   the associated resources freed. If the file was previously unlinked, the\n+ *   file itself is deleted when the last reference is dropped.\n+ *\n+ *   Except when close() fails with EBADF, the file descriptor is released even\n+ *   when close() returns an error, because it is released before the flush\n+ *   that may fail. POSIX leaves the state of the descriptor unspecified after\n+ *   EINTR. Retrying close() after an error may close an unrelated file\n+ *   descriptor that another thread has since been given.\n+ *\n+ *   Errors returned from close() come only from the file's -\u003eflush() method,\n+ *   called by filp_flush(). Errors from -\u003erelease() are not reported.\n+ *   Filesystems without a -\u003eflush() method, such as ext4, xfs and btrfs, never\n+ *   report write errors from close(). Network filesystems such as NFS, CIFS and\n+ *   FUSE implement -\u003eflush() and report deferred write errors (EIO, ENOSPC,\n+ *   EDQUOT) at close time. A successful return does not mean the data reached\n+ *   storage; call fsync() before close() for that.\n+ *\n+ *   On close, the following cleanup operations are performed: the -\u003eflush()\n+ *   method is called if the file has one, POSIX advisory locks are removed,\n+ *   dnotify registrations are cleaned up, and the file reference is released.\n+ *   If this was the last reference, additional cleanup includes: fsnotify close\n+ *   notification, epoll cleanup, OFD, flock and lease removal, FASYNC cleanup,\n+ *   the -\u003erelease() method, and the file structure deallocation.\n+ *\n+ * contexts: process, sleepable\n+ *\n+ * param: fd\n+ *   type: fd, input\n+ *   constraint-type: range(0, INT_MAX)\n+ *   cdesc: Must be a valid, open file descriptor for the current process.\n+ *     The value 0, 1, or 2 (stdin, stdout, stderr) may be closed like any other\n+ *     fd, though this is unusual and may cause issues with libraries that assume\n+ *     these descriptors are valid. The parameter is unsigned int to match kernel\n+ *     file descriptor table indexing. A value that is not open, including any\n+ *     value at or above the current table size, fails with EBADF.\n+ *\n+ * return:\n+ *   type: int\n+ *   check-type: exact\n+ *   success: 0\n+ *   desc: Returns 0 on success. On error, returns a negative error code. Except\n+ *     for EBADF, the file descriptor is still closed when an error is returned\n+ *     and must not be used again. The error comes from the file's -\u003eflush()\n+ *     method, not from the fd remaining open. With EBADF, nothing was released.\n+ *\n+ * error: EBADF, Bad file descriptor\n+ *   desc: fd is at or above the file descriptor table size, has no file\n+ *     assigned (including a slot reserved by a concurrent open() that has not\n+ *     installed its file yet), or was already closed. This is the only error\n+ *     for which no file descriptor was released.\n+ *\n+ * error: EINTR, Interrupted system call\n+ *   desc: The flush operation was interrupted by a signal before completion.\n+ *     This occurs when a driver's -\u003eflush() method (for example wdm_flush() in\n+ *     drivers/usb/class/cdc-wdm.c) performs an interruptible wait that receives\n+ *     a signal. The file descriptor is still released and must not be used\n+ *     again. Kernel-internal restart codes (ERESTARTSYS,\n+ *     ERESTARTNOINTR, ERESTARTNOHAND, ERESTART_RESTARTBLOCK) are converted to\n+ *     EINTR because restarting the syscall would be incorrect once the fd is\n+ *     freed.\n+ *\n+ * error: EIO, I/O error\n+ *   desc: The file's -\u003eflush() method reported an I/O error, typically a\n+ *     deferred write error on a network filesystem such as NFS, CIFS or FUSE,\n+ *     or an error from a driver's -\u003eflush(). Previously buffered write data\n+ *     may have been lost.\n+ *\n+ * error: ENOSPC, No space left on device\n+ *   desc: The file's -\u003eflush() method reported that there was insufficient\n+ *     space to flush buffered writes, for example on NFS when the server runs\n+ *     out of space between write() and close().\n+ *\n+ * error: EDQUOT, Disk quota exceeded\n+ *   desc: The file's -\u003eflush() method reported that the user's disk quota was\n+ *     exceeded while flushing buffered writes, for example on NFS when the\n+ *     quota is exceeded between write() and close().\n+ *\n+ * lock: files-\u003efile_lock\n+ *   type: spinlock\n+ *   acquired: true\n+ *   released: true\n+ *   desc: Taken by file_close_fd() to look up and clear the fd slot atomically,\n+ *     so two concurrent close() calls on one fd cannot both obtain the struct\n+ *     file. Dropped before -\u003eflush() and the final fput. From then on the fd\n+ *     number may be handed out again. An fd reserved by a concurrent open() but\n+ *     not yet installed has a NULL slot, so close() returns EBADF.\n+ *\n+ * lock: file-\u003ef_lock\n+ *   type: spinlock\n+ *   acquired: true\n+ *   released: true\n+ *   desc: Taken from __fput() on the last reference, by eventpoll_release_file()\n+ *     when the file is registered with epoll, and by fasync_remove_entry()\n+ *     (through the -\u003efasync() method) when FASYNC is set. Protects the epoll\n+ *     and fasync links of the file.\n+ *\n+ * lock: ep-\u003emtx\n+ *   type: mutex\n+ *   acquired: true\n+ *   released: true\n+ *   desc: Acquired during epoll cleanup if the file was monitored by epoll.\n+ *     Used to safely remove the file from epoll interest lists.\n+ *\n+ * lock: flc_lock\n+ *   type: spinlock\n+ *   acquired: true\n+ *   released: true\n+ *   desc: File lock context spinlock. Taken by locks_remove_posix() from\n+ *     filp_flush() on every close when the file has POSIX locks, and by\n+ *     locks_remove_file() from __fput() on the last reference to remove OFD,\n+ *     flock, and lease locks.\n+ *\n+ * signal: pending_signals\n+ *   direction: receive\n+ *   action: return\n+ *   condition: When close-time flush performs interruptible wait\n+ *   desc: If the close-time -\u003eflush() method (for example wdm_flush() in\n+ *     cdc-wdm) performs an interruptible wait and a signal is pending, the wait\n+ *     is interrupted. Any kernel restart codes are converted to EINTR since\n+ *     close cannot be restarted after the fd is freed.\n+ *   errno: -EINTR\n+ *   timing: during\n+ *   restartable: no\n+ *\n+ * side-effect: resource_destroy | irreversible\n+ *   target: File descriptor table entry\n+ *   desc: The file descriptor is removed from the process's file descriptor\n+ *     table, making the fd number available for reuse by subsequent open(),\n+ *     dup(), or similar calls. This happens before the flush that may fail, so\n+ *     an error return does not undo it.\n+ *   condition: Always (when fd is valid)\n+ *   reversible: no\n+ *\n+ * side-effect: lock_release\n+ *   target: POSIX advisory locks, OFD locks, flock locks\n+ *   desc: POSIX locks held by this process on the inode are removed on every\n+ *     close via locks_remove_posix() in filp_flush(), except for O_PATH files.\n+ *     OFD and flock locks are removed via locks_remove_file() in __fput() only\n+ *     when this is the last reference to the open file description.\n+ *   condition: Any close of a non-O_PATH file for POSIX locks, last reference\n+ *     for OFD and flock locks\n+ *   reversible: no\n+ *\n+ * side-effect: resource_destroy\n+ *   target: File leases\n+ *   desc: Any file leases held on the file are removed during locks_remove_file()\n+ *     when this is the last reference to the open file description.\n+ *   condition: File had leases and this is the last reference\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: dnotify registrations\n+ *   desc: Directory notification (dnotify) registrations associated with this\n+ *     file are cleaned up via dnotify_flush(). This only applies to directories.\n+ *   condition: File is a directory with dnotify registrations\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: epoll interest lists\n+ *   desc: If the file was being monitored by epoll instances, it is removed\n+ *     from those interest lists via eventpoll_release(), which runs from\n+ *     __fput() only on the last reference. While other references remain, the\n+ *     epoll registrations stay active.\n+ *   condition: File was added to epoll instances and this is the last reference\n+ *   reversible: no\n+ *\n+ * side-effect: filesystem\n+ *   target: Buffered data\n+ *   desc: The file's -\u003eflush() method runs before the file reference is\n+ *     dropped (for example on NFS, CIFS and FUSE) and may return errors such as\n+ *     EIO, ENOSPC or EDQUOT.\n+ *   condition: The filesystem or driver provides a -\u003eflush() method\n+ *   reversible: no\n+ *\n+ * side-effect: free_memory\n+ *   target: struct file and related structures\n+ *   desc: When this is the last reference to the file, the file structure is\n+ *     freed and the dentry and mount references are released.\n+ *   condition: This is the last reference to the file\n+ *   reversible: no\n+ *\n+ * side-effect: filesystem\n+ *   target: Unlinked file deletion\n+ *   desc: If the file was previously unlinked (deleted) but kept open, closing\n+ *     the last reference causes the actual file data to be removed from the\n+ *     filesystem and the inode to be freed.\n+ *   condition: File was unlinked and this is the last reference\n+ *   reversible: no\n+ *\n+ * state-trans: file_descriptor\n+ *   from: open\n+ *   to: closed/free\n+ *   condition: Valid fd passed to close\n+ *   desc: The file descriptor transitions from open (usable) to closed (invalid).\n+ *     The fd number becomes available for reuse.\n+ *\n+ * state-trans: file_reference_count\n+ *   from: n\n+ *   to: n-1 (or freed if n was 1)\n+ *   condition: Always on successful fd lookup\n+ *   desc: The file's reference count is decremented. If this was the last\n+ *     reference, the file is fully cleaned up and freed.\n+ *\n+ * examples: close(fd);  // Ignoring the result loses -\u003eflush() errors\n+ *   if (close(fd) == -1) perror(\"close\");  // Log errors for debugging\n+ *   fsync(fd); close(fd);  // Ensure data persistence before closing\n+ *\n+ * notes: close() drops its reference with fput_close_sync(), so when it is the last\n+ *   reference __fput() runs synchronously in the calling task before close()\n+ *   returns, instead of being deferred to task work.\n+ *\n+ *   Calling close() on a file descriptor while another thread is using it\n+ *   (e.g., in a blocking read() or write()) does not interrupt the blocked\n+ *   operation. The blocked operation continues on the underlying file and\n+ *   may complete even after close() returns.\n+ */\n /*\n  * Careful here! We test whether the file pointer is NULL before\n  * releasing the fd. This ensures that one clone task can't release\ndiff --git a/fs/read_write.c b/fs/read_write.c\nindex e8c14e2760b20..a9c07c21f5888 100644\n--- a/fs/read_write.c\n+++ b/fs/read_write.c\n@@ -720,6 +720,303 @@ ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)\n \treturn ret;\n }\n \n+/**\n+ * sys_read - Read data from a file descriptor\n+ * @fd: File descriptor to read from\n+ * @buf: User-space buffer to read data into\n+ * @count: Maximum number of bytes to read\n+ *\n+ * long-desc: Reads at most count bytes from fd into the user buffer buf. For\n+ *   seekable files (regular files, block devices), the read begins at the\n+ *   current file offset, and the file offset is advanced by the number of\n+ *   bytes read. For stream files (FMODE_STREAM, such as pipes, FIFOs, and\n+ *   sockets), the file offset is not used. Other files, including many\n+ *   character devices, keep a file offset although a driver may ignore it.\n+ *\n+ *   If count is zero, the usual checks still run and the file's read method is\n+ *   still called. Common implementations (regular files, pipes, sockets)\n+ *   return zero without transferring data.\n+ *\n+ *   On success the number of bytes read is returned; zero means end of file\n+ *   for regular files. A read can return fewer bytes than requested, for\n+ *   example near end of file, on a pipe, socket or terminal with less data\n+ *   pending, or when a signal arrives after some data was copied.\n+ *\n+ *   On Linux, read() transfers at most MAX_RW_COUNT (INT_MAX \u0026 PAGE_MASK,\n+ *   0x7ffff000 with 4KB pages, just under 2GB) bytes per call, regardless of\n+ *   whether the filesystem would allow more. This is to avoid issues with\n+ *   signed arithmetic overflow on 32-bit systems.\n+ *\n+ *   POSIX allows reads that are interrupted after reading some data to either\n+ *   return -1 (with errno set to EINTR) or return the number of bytes already\n+ *   read. Linux follows the latter behavior: if data has been read before a\n+ *   signal arrives, the call returns the bytes read rather than failing.\n+ *\n+ * contexts: process, sleepable\n+ *\n+ * param: fd\n+ *   type: fd, input\n+ *   constraint-type: range(0, INT_MAX)\n+ *   cdesc: Must be a valid, open file descriptor with read permission.\n+ *     The file must have been opened with O_RDONLY or O_RDWR. Special values\n+ *     like AT_FDCWD are not valid. File descriptors for directories return\n+ *     EISDIR. Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are\n+ *     valid if open and readable.\n+ *\n+ * param: buf\n+ *   type: user_ptr, output\n+ *   constraint-type: buffer(2)\n+ *   cdesc: Must point to a writable user-space region of at least count bytes.\n+ *     The range is checked via access_ok() and a range outside user space\n+ *     fails with EFAULT. NULL is not rejected by that check. With a count of 0\n+ *     the call returns 0 in common cases. With a count above 0 the copy fails\n+ *     with EFAULT, or the call returns a short count if some data was copied\n+ *     first. The buffer may be partially written if an error occurs mid-read.\n+ *     O_DIRECT reads may require block-size alignment (see STATX_DIOALIGN).\n+ *\n+ * param: count\n+ *   type: uint, input\n+ *   cdesc: Maximum number of bytes to read. The range buf to buf + count must\n+ *     lie within user space or access_ok() fails with EFAULT, which includes\n+ *     counts that do not fit in ssize_t on 64-bit kernels. A count of 0 passes\n+ *     the same checks and the file's read method is still called, typically\n+ *     returning 0 without transferring data. After access_ok() and\n+ *     rw_verify_area() succeed, counts above MAX_RW_COUNT (INT_MAX \u0026\n+ *     PAGE_MASK, 0x7ffff000 with 4KB pages) are clamped to MAX_RW_COUNT.\n+ *\n+ * return:\n+ *   type: int\n+ *   check-type: range\n+ *   success: \u003e= 0\n+ *   desc: On success, returns the number of bytes read (non-negative). Zero\n+ *     indicates end-of-file (EOF) for regular files, or no data available\n+ *     from a device that does not block. The return value may be less than\n+ *     count if fewer bytes were available (short read). Partial reads are\n+ *     not errors. On error, returns a negative error code.\n+ *\n+ * error: EBADF, Bad file descriptor\n+ *   desc: fd is not a valid file descriptor, or fd was not opened for reading.\n+ *     This includes file descriptors opened with O_WRONLY, O_PATH, or file\n+ *     descriptors that have been closed. Also returned if the file structure\n+ *     does not have FMODE_READ set.\n+ *\n+ * error: EFAULT, Bad address\n+ *   desc: buf points outside the accessible address space. The buffer address\n+ *     failed access_ok() validation. Can also occur if a fault happens during\n+ *     copy_to_user() when transferring data to user space after the read\n+ *     completes in kernel space.\n+ *\n+ * error: EINVAL, Invalid argument\n+ *   desc: Returned in several cases: (1) The file has no read or read_iter\n+ *     method (FMODE_CAN_READ is not set). (2) The file was opened with\n+ *     O_DIRECT and the buffer alignment, offset, or count does not meet the\n+ *     filesystem's alignment requirements. (3) For timerfd file descriptors,\n+ *     the buffer is smaller than 8 bytes. (4) rw_verify_area() rejects a\n+ *     negative file position, or a position plus count that overflows, on a\n+ *     file without FOP_UNSIGNED_OFFSET. (5) The count, cast to ssize_t, is\n+ *     negative (32-bit kernels only).\n+ *\n+ * error: EISDIR, Is a directory\n+ *   desc: fd refers to a directory. Directories cannot be read using read();\n+ *     use getdents64() instead. This error is returned by the generic_read_dir()\n+ *     handler installed for directory file operations.\n+ *\n+ * error: EAGAIN, Resource temporarily unavailable\n+ *   desc: fd refers to a file (pipe, socket, device) that is marked non-blocking\n+ *     (O_NONBLOCK) and the read would block. Equivalent to EWOULDBLOCK. The\n+ *     application should retry the read later or use select/poll/epoll.\n+ *\n+ * error: EINTR, Interrupted system call\n+ *   desc: The call was interrupted by a signal before any data was read. This\n+ *     only occurs if no data has been transferred; if some data was read before\n+ *     the signal, the call returns the number of bytes read. The caller should\n+ *     typically restart the read.\n+ *\n+ * error: EIO, Input/output error\n+ *   desc: A low-level I/O error occurred. For regular files, this typically\n+ *     indicates a hardware error on the storage device, a filesystem error,\n+ *     or a network filesystem timeout. For terminals, it is returned when a\n+ *     background process group reads its controlling terminal and SIGTTIN is\n+ *     ignored or blocked, or the process group is orphaned.\n+ *\n+ * error: EOVERFLOW, Value too large for defined data type\n+ *   desc: Returned by rw_verify_area() only for files with FOP_UNSIGNED_OFFSET\n+ *     (for example /proc/pid/mem) when the file position is negative, meaning\n+ *     above LLONG_MAX, and count is at least -pos so that the read would wrap\n+ *     past the end of the 64-bit offset space. Files without\n+ *     FOP_UNSIGNED_OFFSET get EINVAL for a negative position or a position\n+ *     plus count that overflows.\n+ *\n+ * error: ENOBUFS, No buffer space available\n+ *   desc: Returned when reading from pipe-based watch queues (CONFIG_WATCH_QUEUE)\n+ *     when the buffer is too small to hold a complete notification, or when\n+ *     reading packets from pipes with PIPE_BUF_FLAG_WHOLE set.\n+ *\n+ * error: ERESTARTSYS, Restart system call (internal)\n+ *   desc: Internal error code indicating the syscall should be restarted. This\n+ *     is typically translated to EINTR if SA_RESTART is not set on the signal\n+ *     handler, or the syscall is transparently restarted if SA_RESTART is set.\n+ *     User space should not see this error code directly.\n+ *\n+ * error: EACCES, Permission denied\n+ *   desc: The security subsystem (LSM such as SELinux or AppArmor) denied\n+ *     the read operation via security_file_permission(). This can occur even\n+ *     if the file was successfully opened, as LSM policies may enforce checks\n+ *     on each operation.\n+ *\n+ * error: EPERM, Operation not permitted\n+ *   desc: Returned by fanotify permission events (CONFIG_FANOTIFY_ACCESS_PERMISSIONS)\n+ *     when a user-space fanotify listener denies the read operation via\n+ *     fsnotify_file_area_perm().\n+ *\n+ * error: ENODATA, No data available\n+ *   desc: Returned when a filesystem built on netfs (for example AFS or Ceph)\n+ *     ends a read subrequest short without making progress.\n+ *\n+ * error: EOPNOTSUPP, Operation not supported\n+ *   desc: Returned by the read method of the file, not by the generic VFS\n+ *     path. For example, a socket whose protocol has no receive method\n+ *     (sock_no_recvmsg) fails with EOPNOTSUPP. A file with no read method at\n+ *     all fails with EINVAL instead.\n+ *\n+ * lock: file-\u003ef_pos_lock\n+ *   type: mutex\n+ *   acquired: true\n+ *   released: true\n+ *   desc: For regular files that require atomic position updates (FMODE_ATOMIC_POS),\n+ *     the f_pos_lock mutex is acquired by fdget_pos() at syscall entry and released\n+ *     by fdput_pos() at syscall exit. This serializes concurrent reads that share\n+ *     the same file description. Not acquired for files opened with FMODE_STREAM\n+ *     (pipes, sockets) or when the file is not shared.\n+ *\n+ * lock: Filesystem-specific locks\n+ *   type: custom\n+ *   acquired: true\n+ *   released: true\n+ *   desc: The filesystem's read_iter or read method may acquire additional locks.\n+ *     For regular files, this typically includes the inode's i_rwsem for certain\n+ *     operations. For pipes, the pipe-\u003emutex is acquired. For sockets, socket\n+ *     lock is acquired. These are internal to the file operation and released\n+ *     before return.\n+ *\n+ * lock: RCU read-side\n+ *   type: rcu\n+ *   acquired: true\n+ *   released: true\n+ *   desc: Taken only when the files_struct is shared with other threads, in\n+ *     which case the fd lookup in fdget() uses RCU and takes a file reference.\n+ *     A private table needs no RCU. The RCU read lock is acquired and released\n+ *     internally by the fd lookup path, not held across the entire syscall.\n+ *     fdput() releases the file reference count, not the RCU lock.\n+ *\n+ * signal: Any signal\n+ *   direction: receive\n+ *   action: return\n+ *   condition: When blocked waiting for data on interruptible operations\n+ *   desc: The syscall may be interrupted by signals while waiting for data to\n+ *     become available (pipes, sockets, terminals). If interrupted before any\n+ *     data is read, returns -EINTR or -ERESTARTSYS. If data has already been\n+ *     read, returns the number of bytes read.\n+ *   errno: -EINTR\n+ *   timing: during\n+ *   restartable: yes\n+ *\n+ * side-effect: file_position\n+ *   target: file-\u003ef_pos\n+ *   condition: For seekable files when read succeeds (returns \u003e 0)\n+ *   desc: The file offset (f_pos) is advanced by the number of bytes read.\n+ *     For stream files (FMODE_STREAM such as pipes and sockets), the offset\n+ *     is not used or modified. The offset update is protected by f_pos_lock\n+ *     when the file is shared between threads/processes.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: inode access time (atime)\n+ *   condition: When the read_iter implementation calls file_accessed() and\n+ *     O_NOATIME is not set\n+ *   desc: The filesystem's read_iter implementation updates atime via\n+ *     file_accessed() and touch_atime(). Buffered reads (filemap_read) do this\n+ *     after the data loop, including at EOF and after errors, but not for a\n+ *     count of 0. O_DIRECT reads (generic_file_read_iter) do it before the\n+ *     I/O. The update may be suppressed by mount options (noatime, relatime),\n+ *     the O_NOATIME flag, or if the filesystem does not support atime.\n+ *     Relatime updates atime only if it is older than mtime or ctime, or more\n+ *     than a day old.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: task I/O accounting\n+ *   condition: When CONFIG_TASK_XACCT is enabled and the read method is invoked\n+ *   desc: Updates the current task's I/O accounting statistics. The rchar field\n+ *     (read characters) is incremented by bytes read via add_rchar() only on\n+ *     successful reads (ret \u003e 0). The syscr field (syscall read count) is\n+ *     incremented via inc_syscr() once the read method has been invoked, even\n+ *     if it fails, but not when the FMODE_READ, FMODE_CAN_READ, access_ok() or\n+ *     rw_verify_area() checks fail. These statistics are visible in\n+ *     /proc/[pid]/io.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: fsnotify events\n+ *   condition: When read returns \u003e 0\n+ *   desc: Generates an FS_ACCESS fsnotify event via fsnotify_access() allowing\n+ *     inotify, fanotify, and dnotify watchers to be notified of the read. This\n+ *     occurs after data transfer completes successfully.\n+ *   reversible: no\n+ *\n+ * constraint: MAX_RW_COUNT\n+ *   desc: After the access_ok() and rw_verify_area() checks pass, the count\n+ *     parameter is silently clamped to MAX_RW_COUNT (INT_MAX \u0026 PAGE_MASK, just\n+ *     under 2GB) to prevent integer overflow in internal calculations. This is\n+ *     transparent to the caller. The syscall succeeds but reads at most\n+ *     MAX_RW_COUNT bytes.\n+ *   expr: actual_count = min(count, MAX_RW_COUNT)\n+ *\n+ * constraint: File must be open for reading\n+ *   desc: The file descriptor must have been opened with O_RDONLY or O_RDWR.\n+ *     Files opened with O_WRONLY or O_PATH lack FMODE_READ and return EBADF.\n+ *     Files that lack FMODE_CAN_READ because they have no read or read_iter\n+ *     method return EINVAL.\n+ *   expr: (file-\u003ef_mode \u0026 FMODE_READ) \u0026\u0026 (file-\u003ef_mode \u0026 FMODE_CAN_READ)\n+ *\n+ * examples: n = read(fd, buf, sizeof(buf));  // Basic read\n+ *   n = read(STDIN_FILENO, buf, 1024);  // Read from stdin\n+ *   while ((n = read(fd, buf, 4096)) \u003e 0) { process(buf, n); }  // Read loop\n+ *   if (read(fd, buf, count) == 0) { handle_eof(); }  // Check for EOF\n+ *\n+ * notes: The behavior of read() varies significantly depending on the type of\n+ *   file descriptor:\n+ *\n+ *   - Regular files: Reads from current position, advances position, returns 0\n+ *     at EOF. Short reads are rare but possible near EOF or on signal.\n+ *\n+ *   - Pipes and FIFOs: Blocking by default. Returns available data (up to count)\n+ *     or blocks until data is available. Returns 0 when all writers have closed.\n+ *     O_NONBLOCK returns EAGAIN when empty instead of blocking.\n+ *\n+ *   - Sockets: Similar to pipes. Specific behavior depends on socket type and\n+ *     protocol. MSG_* flags can be specified via recv() for more control.\n+ *\n+ *   - Terminals: Line-buffered in canonical mode; read returns when newline is\n+ *     entered or buffer is full. Raw mode returns immediately when data available.\n+ *     Special handling for signals (SIGINT on Ctrl+C, etc.).\n+ *\n+ *   - Device special files: Behavior is device-specific. Some devices support\n+ *     seeking, others do not. Read size may be constrained by device.\n+ *\n+ *   Race condition: Concurrent reads from the same file description (not just\n+ *   file descriptor) can race on the file position. Linux 3.14+ provides atomic\n+ *   position updates for regular files via f_pos_lock, but applications should\n+ *   use pread() for concurrent positioned reads.\n+ *\n+ *   O_DIRECT reads bypass the page cache and typically require aligned buffers\n+ *   and positions. Alignment requirements are filesystem-specific; use statx()\n+ *   with STATX_DIOALIGN (Linux 6.1+) to query. Unaligned O_DIRECT reads fail\n+ *   with EINVAL on most filesystems.\n+ *\n+ *   For splice(2)-like zero-copy reads, consider using splice(), sendfile(),\n+ *   or copy_file_range() instead of read() + write().\n+ */\n SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)\n {\n \treturn ksys_read(fd, buf, count);\n@@ -744,6 +1041,419 @@ ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)\n \treturn ret;\n }\n \n+/**\n+ * sys_write - Write data to a file descriptor\n+ * @fd: File descriptor to write to\n+ * @buf: User-space buffer containing data to write\n+ * @count: Maximum number of bytes to write\n+ *\n+ * long-desc: Writes at most count bytes from the user buffer buf to fd. For\n+ *   seekable files (regular files, block devices), the write begins at the\n+ *   current file offset, and the file offset is advanced by the number of\n+ *   bytes written. If the file was opened with O_APPEND, the file offset is\n+ *   first set to the end of the file before writing. For stream files\n+ *   (FMODE_STREAM, such as pipes, FIFOs, and sockets), the file offset is not\n+ *   used and writing occurs at the position defined by the device. Other\n+ *   files, including many character devices, keep a file offset although a\n+ *   driver may ignore it.\n+ *\n+ *   Fewer than count bytes may be written, for example when the filesystem\n+ *   runs out of space, the write reaches RLIMIT_FSIZE, or a signal arrives\n+ *   after some data was written. The caller writes the rest with another\n+ *   write() call.\n+ *\n+ *   On Linux, write() transfers at most MAX_RW_COUNT (INT_MAX \u0026 PAGE_MASK,\n+ *   0x7ffff000 with 4KB pages, just under 2GB) bytes per call, regardless of\n+ *   whether the file or filesystem would allow more. This prevents signed\n+ *   arithmetic overflow.\n+ *\n+ *   For regular files, a successful write() does not guarantee that data has been\n+ *   committed to disk. Use fsync(2) or fdatasync(2) if durability is required.\n+ *   For O_SYNC or O_DSYNC files, the kernel automatically syncs data on write.\n+ *\n+ *   POSIX permits writes that are interrupted after partial writes to either\n+ *   return -1 with errno=EINTR, or to return the count of bytes already written.\n+ *   Linux implements the latter behavior: if some data has been written before\n+ *   a signal arrives, write() returns the number of bytes written rather than\n+ *   failing with EINTR.\n+ *\n+ * contexts: process, sleepable\n+ *\n+ * param: fd\n+ *   type: fd, input\n+ *   constraint-type: range(0, INT_MAX)\n+ *   cdesc: Must be a valid, open file descriptor with write permission.\n+ *     The file must have been opened with O_WRONLY or O_RDWR. File descriptors\n+ *     opened with O_RDONLY, O_PATH, or that have been closed return EBADF.\n+ *     Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are valid if\n+ *     open and writable. AT_FDCWD and other special values are not valid.\n+ *\n+ * param: buf\n+ *   type: user_ptr, input\n+ *   constraint-type: buffer(2)\n+ *   cdesc: Must point to a readable user-space region of at least count bytes.\n+ *     The range is checked via access_ok() and a range outside user space\n+ *     fails with EFAULT. NULL is not rejected by that check. With a count of 0\n+ *     the call returns 0 in common cases. With a count above 0 the copy from\n+ *     user space fails with EFAULT, or the call returns a short count if part\n+ *     of the buffer was copied first. O_DIRECT writes may require block-size\n+ *     alignment (see STATX_DIOALIGN).\n+ *\n+ * param: count\n+ *   type: uint, input\n+ *   cdesc: Maximum number of bytes to write. The range buf to buf + count must\n+ *     lie within user space or access_ok() fails with EFAULT, which includes\n+ *     counts that do not fit in ssize_t on 64-bit kernels. On 32-bit kernels\n+ *     such a count reaches rw_verify_area() and fails with EINVAL. A count of\n+ *     0 is passed to the file's write method, which typically returns 0 but\n+ *     may trigger driver-specific side effects. After the checks pass, counts\n+ *     above MAX_RW_COUNT (INT_MAX \u0026 PAGE_MASK, 0x7ffff000 with 4KB pages) are\n+ *     clamped.\n+ *\n+ * return:\n+ *   type: int\n+ *   check-type: range\n+ *   success: \u003e= 0\n+ *   desc: On success, returns the number of bytes written (non-negative). Zero\n+ *     indicates that nothing was written (count was 0, or a device-specific\n+ *     write method accepted no data). Non-blocking writes that cannot proceed\n+ *     return -EAGAIN instead. The return value may be less than count due to\n+ *     resource limits, signal interruption, or device constraints (short\n+ *     write). On error, returns a negative error code.\n+ *\n+ * error: EBADF, Bad file descriptor\n+ *   desc: fd is not a valid file descriptor, or fd was not opened for writing.\n+ *     This includes file descriptors opened with O_RDONLY, O_PATH, or file\n+ *     descriptors that have been closed. Also returned if the file structure\n+ *     does not have FMODE_WRITE set.\n+ *\n+ * error: EFAULT, Bad address\n+ *   desc: buf points outside the accessible address space. The buffer address\n+ *     failed access_ok() validation. Can also occur if a fault happens during\n+ *     copy_from_user() when reading data from user space.\n+ *\n+ * error: EINVAL, Invalid argument\n+ *   desc: Returned in several cases: (1) The file has no write or write_iter\n+ *     method (FMODE_CAN_WRITE is not set). (2) The file was opened with\n+ *     O_DIRECT and the buffer alignment, offset, or count does not meet the\n+ *     filesystem's alignment requirements. (3) rw_verify_area() rejects a\n+ *     negative file position, or a position plus count that overflows, on a\n+ *     file without FOP_UNSIGNED_OFFSET. (4) The count, cast to ssize_t, is\n+ *     negative (32-bit kernels only, as 64-bit kernels fail access_ok() first\n+ *     with EFAULT).\n+ *\n+ * error: EAGAIN, Resource temporarily unavailable\n+ *   desc: fd refers to a file (pipe, socket, device) that is marked non-blocking\n+ *     (O_NONBLOCK) and the write would block because the buffer is full.\n+ *     Equivalent to EWOULDBLOCK. The application should retry later or use\n+ *     select/poll/epoll to wait for writability.\n+ *\n+ * error: EINTR, Interrupted system call\n+ *   desc: The call was interrupted by a signal before any data was written. This\n+ *     only occurs if no data has been transferred; if some data was written\n+ *     before the signal, the call returns the number of bytes written. The\n+ *     caller should typically restart the write.\n+ *\n+ * error: EPIPE, Broken pipe\n+ *   desc: fd refers to a pipe or socket whose reading end has been closed.\n+ *     When this condition occurs, the calling process also receives a SIGPIPE\n+ *     signal. If the signal is caught or ignored, EPIPE is still returned.\n+ *     For sockets, MSG_NOSIGNAL (via send()) suppresses the signal. For\n+ *     pwritev2(), the RWF_NOSIGNAL flag suppresses it.\n+ *\n+ * error: EFBIG, File too large\n+ *   desc: The write starts at or beyond a file size limit. The generic write\n+ *     path returns EFBIG when the starting position is at or beyond\n+ *     RLIMIT_FSIZE, in which case the process also receives SIGXFSZ, or at or\n+ *     beyond the maximum file size of the filesystem. Without O_LARGEFILE\n+ *     (32-bit systems only) that maximum is 2GB minus one byte. A write that\n+ *     starts below a limit but would cross it is shortened to end at the\n+ *     limit.\n+ *\n+ * error: ENOSPC, No space left on device\n+ *   desc: The device containing the file has no room for the data. This can\n+ *     occur mid-write resulting in a short write followed by ENOSPC on retry.\n+ *\n+ * error: EDQUOT, Disk quota exceeded\n+ *   desc: The user's quota of disk blocks on the filesystem has been exhausted.\n+ *     Like ENOSPC, this can result in a short write.\n+ *\n+ * error: EIO, Input/output error\n+ *   desc: A low-level I/O error occurred while modifying the inode or writing\n+ *     data. This typically indicates hardware failure, filesystem corruption,\n+ *     or network filesystem timeout. Some data may have been written.\n+ *\n+ * error: EPERM, Operation not permitted\n+ *   desc: The operation was prevented (1) by a file seal (F_SEAL_WRITE or\n+ *     F_SEAL_FUTURE_WRITE on memfd/shmem, or F_SEAL_GROW for a write that\n+ *     extends the file), (2) by a filesystem-specific check, for example ext4\n+ *     refuses writes to an immutable inode, (3) because the file is a\n+ *     read-only block device, (4) by an LSM hook denying the operation, or (5)\n+ *     by a fanotify pre-content (FAN_PRE_ACCESS) listener denying the write.\n+ *\n+ * error: EOVERFLOW, Value too large for defined data type\n+ *   desc: Returned by rw_verify_area() only for files with FOP_UNSIGNED_OFFSET\n+ *     (for example /proc/pid/mem) when the file position is negative, meaning\n+ *     above LLONG_MAX, and count is at least -pos so that the write would wrap\n+ *     past the end of the 64-bit offset space. Files without\n+ *     FOP_UNSIGNED_OFFSET get EINVAL for a negative position or a position\n+ *     plus count that overflows. Exceeding filesystem file size limits is\n+ *     EFBIG, not EOVERFLOW.\n+ *\n+ * error: EDESTADDRREQ, Destination address required\n+ *   desc: fd is a datagram socket for which no peer address has been set using\n+ *     connect(2). Use sendto(2) to specify the destination address.\n+ *\n+ * error: ETXTBSY, Text file busy\n+ *   desc: The file is being used as a swap file (IS_SWAPFILE).\n+ *\n+ * error: EXDEV, Cross-device link\n+ *   desc: When writing to a pipe that has been configured as a watch queue\n+ *     (CONFIG_WATCH_QUEUE), direct write() calls are not supported.\n+ *\n+ * error: ENOMEM, Out of memory\n+ *   desc: Insufficient kernel memory was available for the write operation.\n+ *     For pipes, this occurs when allocating pages for the pipe buffer.\n+ *\n+ * error: ERESTARTSYS, Restart system call (internal)\n+ *   desc: Internal error code indicating the syscall should be restarted. This\n+ *     is converted to EINTR if SA_RESTART is not set on the signal handler, or\n+ *     the syscall is transparently restarted if SA_RESTART is set. User space\n+ *     should not see this error code directly.\n+ *\n+ * error: EACCES, Permission denied\n+ *   desc: The security subsystem (LSM such as SELinux or AppArmor) denied the\n+ *     write operation via security_file_permission(). This can occur even if\n+ *     the file was successfully opened.\n+ *\n+ * lock: file-\u003ef_pos_lock\n+ *   type: mutex\n+ *   acquired: true\n+ *   released: true\n+ *   desc: For regular files that require atomic position updates (FMODE_ATOMIC_POS),\n+ *     the f_pos_lock mutex is acquired by fdget_pos() at syscall entry and released\n+ *     by fdput_pos() at syscall exit. This serializes concurrent writes sharing\n+ *     the same file description. Not acquired for stream files (FMODE_STREAM like\n+ *     pipes and sockets) or when the file is not shared.\n+ *\n+ * lock: sb-\u003es_writers (freeze protection)\n+ *   type: custom\n+ *   acquired: true\n+ *   released: true\n+ *   desc: For regular files, file_start_write() acquires freeze protection on\n+ *     the superblock via sb_start_write() before the write, and file_end_write()\n+ *     releases it after. This prevents writes during filesystem freeze. Not\n+ *     acquired for non-regular files (pipes, sockets, devices).\n+ *\n+ * lock: inode-\u003ei_rwsem\n+ *   type: semaphore\n+ *   acquired: true\n+ *   released: true\n+ *   desc: For regular files using generic_file_write_iter(), the inode's i_rwsem\n+ *     is acquired in write mode before modifying file data. This is internal to\n+ *     the filesystem and released before return. Not all filesystems use this\n+ *     pattern.\n+ *\n+ * lock: pipe-\u003emutex\n+ *   type: mutex\n+ *   acquired: true\n+ *   released: true\n+ *   desc: For pipes and FIFOs, the pipe's mutex is held while modifying pipe\n+ *     buffers. Released temporarily while waiting for space, then reacquired.\n+ *\n+ * lock: RCU read-side\n+ *   type: rcu\n+ *   acquired: true\n+ *   released: true\n+ *   desc: Taken only when the files_struct is shared with other threads, in\n+ *     which case the fd lookup in fdget() uses RCU and takes a file reference.\n+ *     A private table needs no RCU. The RCU read lock is acquired and released\n+ *     internally by the fd lookup path, not held across the entire syscall.\n+ *     fdput() releases the file reference count, not the RCU lock.\n+ *\n+ * signal: SIGPIPE\n+ *   number: SIGPIPE\n+ *   direction: send\n+ *   action: terminate\n+ *   condition: Writing to a pipe or socket with no readers\n+ *   desc: When writing to a pipe whose read end is closed, or a socket whose\n+ *     peer has closed, SIGPIPE is sent to the calling process. The default\n+ *     action terminates the process. Use signal(SIGPIPE, SIG_IGN) to suppress\n+ *     for write(). EPIPE is returned regardless of signal disposition.\n+ *   timing: during\n+ *\n+ * signal: SIGXFSZ\n+ *   number: SIGXFSZ\n+ *   direction: send\n+ *   action: coredump\n+ *   condition: Write starts at or beyond RLIMIT_FSIZE\n+ *   desc: When a write starts at or beyond the soft file size limit\n+ *     (RLIMIT_FSIZE), generic_write_check_limits() sends SIGXFSZ and the write\n+ *     returns EFBIG. The default action terminates with a core dump. A write\n+ *     that starts below the limit but would cross it is shortened to end at\n+ *     the limit, with no signal. If RLIMIT_FSIZE is RLIM_INFINITY, no signal\n+ *     is sent.\n+ *   timing: during\n+ *\n+ * signal: Any signal\n+ *   direction: receive\n+ *   action: return\n+ *   condition: While blocked waiting for space (pipes, sockets)\n+ *   desc: The syscall may be interrupted by signals while waiting for buffer\n+ *     space to become available. If interrupted before any data is written,\n+ *     returns -EINTR or -ERESTARTSYS. If data was already written, returns the\n+ *     byte count. Restartable if SA_RESTART is set and no data was written.\n+ *   errno: -EINTR\n+ *   timing: during\n+ *   restartable: yes\n+ *\n+ * side-effect: file_position\n+ *   target: file-\u003ef_pos\n+ *   condition: For seekable files when write succeeds (returns \u003e 0)\n+ *   desc: The file offset (f_pos) is advanced by the number of bytes written.\n+ *     For files opened with O_APPEND, f_pos is first set to file size. For\n+ *     stream files (FMODE_STREAM such as pipes and sockets), the offset is not\n+ *     used or modified. Position updates are protected by f_pos_lock when\n+ *     shared.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: inode timestamps (mtime, ctime)\n+ *   condition: Before the data is copied, for a non-zero count on the generic\n+ *     write path\n+ *   desc: Updates the file's modification time (mtime) and change time (ctime)\n+ *     via file_update_time(), which runs before the data is copied, so the\n+ *     timestamps can change even if the write then fails or is short. The\n+ *     update is skipped for inodes flagged NOCMTIME and when the timestamps\n+ *     are unchanged. The timestamp precision depends on whether the filesystem\n+ *     type sets FS_MGTIME (multigrain timestamps).\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: SUID/SGID bits (mode) and file capabilities\n+ *   condition: Write to a regular file that has S_ISUID, S_ISGID, or file\n+ *     capabilities set, with a non-zero count\n+ *   desc: file_remove_privs() clears S_ISUID, and clears S_ISGID when S_IXGRP\n+ *     is also set or the caller is not in the file's group, unless the caller\n+ *     has CAP_FSETID. File capabilities (the security.capability xattr) are\n+ *     removed as well, regardless of CAP_FSETID. This is a security feature to\n+ *     prevent privilege escalation via modified setuid binaries. It runs before\n+ *     the data is copied.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: file data\n+ *   condition: When write succeeds (returns \u003e 0)\n+ *   desc: Modifies the file's data content. For regular files, data is written\n+ *     to the page cache (buffered I/O) or directly to storage (O_DIRECT).\n+ *     Data is not guaranteed to be persistent until fsync() or fdatasync()\n+ *     completes, or writeback has written it to storage.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: task I/O accounting\n+ *   condition: When CONFIG_TASK_XACCT is enabled and the write method is\n+ *     invoked\n+ *   desc: Updates the current task's I/O accounting statistics. The wchar field\n+ *     (write characters) is incremented by bytes written via add_wchar() only on\n+ *     successful writes (ret \u003e 0). The syscw field (syscall write count) is\n+ *     incremented via inc_syscw() once the write method has been invoked, even\n+ *     if it fails, but not when the FMODE_WRITE, FMODE_CAN_WRITE, access_ok()\n+ *     or rw_verify_area() checks fail. These statistics are visible in\n+ *     /proc/[pid]/io.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: fsnotify events\n+ *   condition: When write returns \u003e 0\n+ *   desc: Generates an FS_MODIFY fsnotify event via fsnotify_modify(), allowing\n+ *     inotify, fanotify, and dnotify watchers to be notified of the write.\n+ *\n+ * capability: CAP_FSETID\n+ *   type: bypass_check\n+ *   allows: Keep the SUID and SGID bits set when the file is written\n+ *   without: SUID is cleared on write, and SGID is cleared if S_IXGRP is set\n+ *     or the caller is not in the file's group\n+ *   condition: Checked in setattr_should_drop_suidgid() during\n+ *     file_remove_privs()\n+ *\n+ * constraint: MAX_RW_COUNT\n+ *   desc: After the access_ok() and rw_verify_area() checks pass, the count\n+ *     parameter is silently clamped to MAX_RW_COUNT (INT_MAX \u0026 PAGE_MASK, just\n+ *     under 2GB) to prevent integer overflow in internal calculations. This is\n+ *     transparent to the caller.\n+ *   expr: actual_count = min(count, MAX_RW_COUNT)\n+ *\n+ * constraint: File must be open for writing\n+ *   desc: The file descriptor must have been opened with O_WRONLY or O_RDWR.\n+ *     Files opened with O_RDONLY or O_PATH cannot be written and return EBADF.\n+ *     The file must have both FMODE_WRITE and FMODE_CAN_WRITE flags set.\n+ *   expr: (file-\u003ef_mode \u0026 FMODE_WRITE) \u0026\u0026 (file-\u003ef_mode \u0026 FMODE_CAN_WRITE)\n+ *\n+ * constraint: RLIMIT_FSIZE\n+ *   desc: The size of data written is constrained by the RLIMIT_FSIZE resource\n+ *     limit. In generic_write_check_limits(), a write that starts at or beyond\n+ *     the limit sends SIGXFSZ and returns EFBIG. A write that starts below the\n+ *     limit but would cross it is shortened to limit - pos bytes, with no\n+ *     signal.\n+ *   expr: pos \u003c rlimit(RLIMIT_FSIZE) || rlimit(RLIMIT_FSIZE) == RLIM_INFINITY\n+ *\n+ * constraint: File seals\n+ *   desc: For memfd or shmem files with F_SEAL_WRITE or F_SEAL_FUTURE_WRITE\n+ *     seals applied, all write operations fail with EPERM. With F_SEAL_GROW,\n+ *     writes that would extend file size fail with EPERM.\n+ *\n+ * examples: n = write(fd, buf, sizeof(buf));  // Basic write\n+ *   n = write(STDOUT_FILENO, msg, strlen(msg));  // Write to stdout\n+ *   // Handle short writes:\n+ *   while (total \u003c len) {\n+ *     n = write(fd, buf + total, len - total);\n+ *     if (n \u003c 0) break;\n+ *     total += n;\n+ *   }\n+ *   // Pipe error handling:\n+ *   if (write(pipefd[1], \u0026byte, 1) \u003c 0 \u0026\u0026 errno == EPIPE)\n+ *     handle_broken_pipe();\n+ *\n+ * notes: The behavior of write() varies significantly depending on the type of\n+ *   file descriptor:\n+ *\n+ *   - Regular files: Writes to the page cache (buffered) or directly to storage\n+ *     (O_DIRECT). Short writes are rare except near RLIMIT_FSIZE or disk full.\n+ *     O_APPEND is atomic for determining write position.\n+ *\n+ *   - Pipes and FIFOs: Blocking by default. Writes up to PIPE_BUF (4096 bytes\n+ *     on Linux) are guaranteed atomic. Larger writes may be interleaved with\n+ *     writes from other processes. Blocks if pipe is full; returns EAGAIN with\n+ *     O_NONBLOCK. SIGPIPE/EPIPE if no readers.\n+ *\n+ *   - Sockets: Behavior depends on socket type and protocol. Stream sockets\n+ *     (TCP) may return partial writes. Datagram sockets (UDP) typically write\n+ *     complete messages or fail. SIGPIPE/EPIPE for broken connections (unless\n+ *     MSG_NOSIGNAL). EDESTADDRREQ for unconnected datagram sockets.\n+ *\n+ *   - Terminals: May block on flow control. Canonical vs raw mode affects\n+ *     behavior. Special characters may be interpreted.\n+ *\n+ *   - Device special files: Behavior is device-specific. Block devices behave\n+ *     similarly to regular files. Character device behavior varies.\n+ *\n+ *   Race condition considerations: Concurrent writes from threads sharing a\n+ *   file description race on the file position. Linux 3.14+ provides atomic\n+ *   position updates via f_pos_lock for regular files (FMODE_ATOMIC_POS), but\n+ *   for maximum safety, use pwrite() for concurrent positioned writes.\n+ *\n+ *   O_DIRECT writes bypass the page cache and typically require buffer and\n+ *   offset alignment to filesystem block size. Query requirements via statx()\n+ *   with STATX_DIOALIGN (Linux 6.1+). Unaligned O_DIRECT writes return EINVAL\n+ *   on most filesystems.\n+ *\n+ *   For zero-copy writes, consider using splice(2), sendfile(2), or vmsplice(2)\n+ *   instead of copying data through user-space buffers with write().\n+ *\n+ *   Partial writes (short writes) must be handled by application code.\n+ *   Applications should loop until all data is written or an error occurs.\n+ */\n SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,\n \t\tsize_t, count)\n {\ndiff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h\nindex b2988aa12f664..b754fa182a6a3 100644\n--- a/include/asm-generic/vmlinux.lds.h\n+++ b/include/asm-generic/vmlinux.lds.h\n@@ -296,6 +296,19 @@\n #define TRACE_SYSCALLS()\n #endif\n \n+#ifdef CONFIG_KAPI_SPEC\n+/*\n+ * .kapi_specs is an array of pointers (see DEFINE_KERNEL_API_SPEC()). Align\n+ * __start_kapi_specs to at least pointer alignment so that no padding\n+ * separates it from the first entry.\n+ */\n+#define KAPI_SPECS()\t\t\t\t\t\t\t\\\n+\t. = ALIGN(8);\t\t\t\t\t\t\t\\\n+\tBOUNDED_SECTION_BY(.kapi_specs, _kapi_specs)\n+#else\n+#define KAPI_SPECS()\n+#endif\n+\n #ifdef CONFIG_BPF_EVENTS\n #define BPF_RAW_TP() STRUCT_ALIGN();\t\t\t\t\\\n \tBOUNDED_SECTION_BY(__bpf_raw_tp_map, __bpf_raw_tp)\n@@ -485,6 +498,7 @@\n \t\t. = ALIGN(8);\t\t\t\t\t\t\\\n \t\tBOUNDED_SECTION_BY(__tracepoints_ptrs, ___tracepoints_ptrs) \\\n \t\t*(__tracepoints_strings)/* Tracepoints: strings */\t\\\n+\t\tKAPI_SPECS()\t\t\t\t\t\t\\\n \t}\t\t\t\t\t\t\t\t\\\n \t\t\t\t\t\t\t\t\t\\\n \t.rodata1          : AT(ADDR(.rodata1) - LOAD_OFFSET) {\t\t\\\ndiff --git a/include/linux/kapi_syscall.h b/include/linux/kapi_syscall.h\nnew file mode 100644\nindex 0000000000000..bc9387e0e052c\n--- /dev/null\n+++ b/include/linux/kapi_syscall.h\n@@ -0,0 +1,35 @@\n+/* SPDX-License-Identifier: GPL-2.0 */\n+/*\n+ * Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+ *\n+ * Spec lookup and syscall validation entry points of the kernel API\n+ * specification framework, kept apart from \u003clinux/kernel_api_spec.h\u003e so that\n+ * \u003clinux/syscalls.h\u003e can declare them cheaply.\n+ */\n+\n+#ifndef _LINUX_KAPI_SYSCALL_H\n+#define _LINUX_KAPI_SYSCALL_H\n+\n+#include \u003clinux/types.h\u003e\n+\n+struct kernel_api_spec;\n+\n+const struct kernel_api_spec *kapi_get_spec(const char *name);\n+\n+#ifdef CONFIG_KAPI_RUNTIME_CHECKS\n+int kapi_validate_syscall_params(const struct kernel_api_spec *spec,\n+\t\t\t\t const s64 *params, int param_count);\n+int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 retval);\n+#else\n+static inline int kapi_validate_syscall_params(const struct kernel_api_spec *spec,\n+\t\t\t\t\t       const s64 *params, int param_count)\n+{\n+\treturn 0;\n+}\n+static inline int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 retval)\n+{\n+\treturn 0;\n+}\n+#endif\n+\n+#endif /* _LINUX_KAPI_SYSCALL_H */\ndiff --git a/include/linux/kernel_api_spec.h b/include/linux/kernel_api_spec.h\nnew file mode 100644\nindex 0000000000000..f72ef5cd73b38\n--- /dev/null\n+++ b/include/linux/kernel_api_spec.h\n@@ -0,0 +1,1226 @@\n+/* SPDX-License-Identifier: GPL-2.0 */\n+/*\n+ * Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+ *\n+ * kernel_api_spec.h - Kernel API Specification Framework\n+ *\n+ * Structures and macros for specifying kernel APIs in a human and\n+ * machine-readable form: parameters, return values, error conditions,\n+ * and constraints.\n+ */\n+\n+#ifndef _LINUX_KERNEL_API_SPEC_H\n+#define _LINUX_KERNEL_API_SPEC_H\n+\n+#include \u003clinux/array_size.h\u003e\n+#include \u003clinux/bits.h\u003e\n+#include \u003clinux/compiler.h\u003e\n+#include \u003clinux/errno.h\u003e\n+#include \u003clinux/kapi_syscall.h\u003e\n+#include \u003clinux/kernel.h\u003e\n+#include \u003clinux/stringify.h\u003e\n+#include \u003clinux/types.h\u003e\n+\n+struct sigaction;\n+\n+#define KAPI_MAX_PARAMS\t\t16\n+#define KAPI_MAX_ERRORS\t\t32\n+#define KAPI_MAX_CONSTRAINTS\t32\n+#define KAPI_MAX_LOCKS\t\t16\n+#define KAPI_MAX_SIGNALS\t32\n+#define KAPI_MAX_NAME_LEN\t128\n+#define KAPI_MAX_DESC_LEN\t512\n+#define KAPI_MAX_CAPABILITIES\t8\n+\n+/* Magic numbers for section validation (ASCII mnemonics) */\n+#define KAPI_MAGIC_PARAMS\t0x4B415031\t/* 'KAP1' */\n+#define KAPI_MAGIC_RETURN\t0x4B415232\t/* 'KAR2' */\n+#define KAPI_MAGIC_ERRORS\t0x4B414533\t/* 'KAE3' */\n+#define KAPI_MAGIC_LOCKS\t0x4B414C34\t/* 'KAL4' */\n+#define KAPI_MAGIC_CONSTRAINTS\t0x4B414335\t/* 'KAC5' */\n+#define KAPI_MAGIC_INFO\t\t0x4B414936\t/* 'KAI6' */\n+#define KAPI_MAGIC_SIGNALS\t0x4B415337\t/* 'KAS7' */\n+#define KAPI_MAGIC_SIGMASK\t0x4B414D38\t/* 'KAM8' */\n+#define KAPI_MAGIC_STRUCTS\t0x4B415439\t/* 'KAT9' */\n+#define KAPI_MAGIC_EFFECTS\t0x4B414641\t/* 'KAFA' */\n+#define KAPI_MAGIC_TRANS\t0x4B415442\t/* 'KATB' */\n+#define KAPI_MAGIC_CAPS\t\t0x4B414343\t/* 'KACC' */\n+\n+/**\n+ * enum kapi_param_type - Parameter type classification\n+ * @KAPI_TYPE_VOID: void type\n+ * @KAPI_TYPE_INT: Integer types (int, long, etc.)\n+ * @KAPI_TYPE_UINT: Unsigned integer types\n+ * @KAPI_TYPE_PTR: Pointer types\n+ * @KAPI_TYPE_STRUCT: Structure types\n+ * @KAPI_TYPE_UNION: Union types\n+ * @KAPI_TYPE_ENUM: Enumeration types\n+ * @KAPI_TYPE_FUNC_PTR: Function pointer types\n+ * @KAPI_TYPE_ARRAY: Array types\n+ * @KAPI_TYPE_FD: File descriptor - range-checked only\n+ * @KAPI_TYPE_USER_PTR: User space pointer - validated for access and size\n+ * @KAPI_TYPE_PATH: Pathname - validated for access and path limits\n+ * @KAPI_TYPE_CUSTOM: Custom/complex types\n+ */\n+enum kapi_param_type {\n+\tKAPI_TYPE_VOID = 0,\n+\tKAPI_TYPE_INT,\n+\tKAPI_TYPE_UINT,\n+\tKAPI_TYPE_PTR,\n+\tKAPI_TYPE_STRUCT,\n+\tKAPI_TYPE_UNION,\n+\tKAPI_TYPE_ENUM,\n+\tKAPI_TYPE_FUNC_PTR,\n+\tKAPI_TYPE_ARRAY,\n+\tKAPI_TYPE_FD,\t\t/* File descriptor - range-checked only */\n+\tKAPI_TYPE_USER_PTR,\t/* User space pointer - validated for access and size */\n+\tKAPI_TYPE_PATH,\t\t/* Pathname - validated for access and path limits */\n+\tKAPI_TYPE_CUSTOM,\n+};\n+\n+/**\n+ * enum kapi_param_flags - Parameter attribute flags\n+ * @KAPI_PARAM_IN: Input parameter\n+ * @KAPI_PARAM_OUT: Output parameter\n+ * @KAPI_PARAM_INOUT: Input/output parameter\n+ * @KAPI_PARAM_OPTIONAL: Optional parameter (can be NULL)\n+ * @KAPI_PARAM_CONST: Const qualified parameter\n+ * @KAPI_PARAM_VOLATILE: Volatile qualified parameter\n+ * @KAPI_PARAM_USER: User space pointer\n+ * @KAPI_PARAM_DMA: DMA-capable memory required\n+ * @KAPI_PARAM_ALIGNED: Alignment requirements\n+ */\n+enum kapi_param_flags {\n+\tKAPI_PARAM_IN\t\t= (1 \u003c\u003c 0),\n+\tKAPI_PARAM_OUT\t\t= (1 \u003c\u003c 1),\n+\tKAPI_PARAM_INOUT\t= (KAPI_PARAM_IN | KAPI_PARAM_OUT),\n+\tKAPI_PARAM_OPTIONAL\t= (1 \u003c\u003c 3),\n+\tKAPI_PARAM_CONST\t= (1 \u003c\u003c 4),\n+\tKAPI_PARAM_VOLATILE\t= (1 \u003c\u003c 5),\n+\tKAPI_PARAM_USER\t\t= (1 \u003c\u003c 6),\n+\tKAPI_PARAM_DMA\t\t= (1 \u003c\u003c 7),\n+\tKAPI_PARAM_ALIGNED\t= (1 \u003c\u003c 8),\n+};\n+\n+/**\n+ * enum kapi_context_flags - Function execution context flags\n+ * @KAPI_CTX_PROCESS: Can be called from process context\n+ * @KAPI_CTX_SOFTIRQ: Can be called from softirq context\n+ * @KAPI_CTX_HARDIRQ: Can be called from hardirq context\n+ * @KAPI_CTX_NMI: Can be called from NMI context\n+ * @KAPI_CTX_ATOMIC: Must be called in atomic context\n+ * @KAPI_CTX_SLEEPABLE: May sleep\n+ * @KAPI_CTX_PREEMPT_DISABLED: Requires preemption disabled\n+ * @KAPI_CTX_IRQ_DISABLED: Requires interrupts disabled\n+ */\n+enum kapi_context_flags {\n+\tKAPI_CTX_PROCESS\t= (1 \u003c\u003c 0),\n+\tKAPI_CTX_SOFTIRQ\t= (1 \u003c\u003c 1),\n+\tKAPI_CTX_HARDIRQ\t= (1 \u003c\u003c 2),\n+\tKAPI_CTX_NMI\t\t= (1 \u003c\u003c 3),\n+\tKAPI_CTX_ATOMIC\t\t= (1 \u003c\u003c 4),\n+\tKAPI_CTX_SLEEPABLE\t= (1 \u003c\u003c 5),\n+\tKAPI_CTX_PREEMPT_DISABLED = (1 \u003c\u003c 6),\n+\tKAPI_CTX_IRQ_DISABLED\t= (1 \u003c\u003c 7),\n+};\n+\n+/**\n+ * enum kapi_lock_type - Lock types used/required by the function\n+ * @KAPI_LOCK_NONE: No locking requirements\n+ * @KAPI_LOCK_MUTEX: Mutex lock\n+ * @KAPI_LOCK_SPINLOCK: Spinlock\n+ * @KAPI_LOCK_RWLOCK: Read-write lock\n+ * @KAPI_LOCK_SEQLOCK: Sequence lock\n+ * @KAPI_LOCK_RCU: RCU lock\n+ * @KAPI_LOCK_SEMAPHORE: Semaphore\n+ * @KAPI_LOCK_CUSTOM: Custom locking mechanism\n+ */\n+enum kapi_lock_type {\n+\tKAPI_LOCK_NONE = 0,\n+\tKAPI_LOCK_MUTEX,\n+\tKAPI_LOCK_SPINLOCK,\n+\tKAPI_LOCK_RWLOCK,\n+\tKAPI_LOCK_SEQLOCK,\n+\tKAPI_LOCK_RCU,\n+\tKAPI_LOCK_SEMAPHORE,\n+\tKAPI_LOCK_CUSTOM,\n+};\n+\n+/**\n+ * enum kapi_constraint_type - Types of parameter constraints\n+ * @KAPI_CONSTRAINT_NONE: No constraint\n+ * @KAPI_CONSTRAINT_RANGE: Numeric range constraint\n+ * @KAPI_CONSTRAINT_MASK: Bitmask constraint\n+ * @KAPI_CONSTRAINT_ENUM: Enumerated values constraint\n+ * @KAPI_CONSTRAINT_ALIGNMENT: Alignment constraint (must be aligned to specified boundary)\n+ * @KAPI_CONSTRAINT_POWER_OF_TWO: Value must be a power of two\n+ * @KAPI_CONSTRAINT_PAGE_ALIGNED: Value must be page-aligned\n+ * @KAPI_CONSTRAINT_NONZERO: Value must be non-zero\n+ * @KAPI_CONSTRAINT_USER_STRING: Userspace null-terminated string with length range\n+ * @KAPI_CONSTRAINT_USER_PATH: Userspace pathname string (validated for accessibility and PATH_MAX)\n+ * @KAPI_CONSTRAINT_USER_PTR: Userspace pointer (validated for accessibility and size)\n+ * @KAPI_CONSTRAINT_BUFFER: Userspace buffer pointer (validated by copy_to/from_user)\n+ * @KAPI_CONSTRAINT_CUSTOM: Custom validation function\n+ */\n+enum kapi_constraint_type {\n+\tKAPI_CONSTRAINT_NONE = 0,\n+\tKAPI_CONSTRAINT_RANGE,\n+\tKAPI_CONSTRAINT_MASK,\n+\tKAPI_CONSTRAINT_ENUM,\n+\tKAPI_CONSTRAINT_ALIGNMENT,\n+\tKAPI_CONSTRAINT_POWER_OF_TWO,\n+\tKAPI_CONSTRAINT_PAGE_ALIGNED,\n+\tKAPI_CONSTRAINT_NONZERO,\n+\tKAPI_CONSTRAINT_USER_STRING,\n+\tKAPI_CONSTRAINT_USER_PATH,\n+\tKAPI_CONSTRAINT_USER_PTR,\n+\tKAPI_CONSTRAINT_BUFFER,\n+\tKAPI_CONSTRAINT_CUSTOM,\n+};\n+\n+/**\n+ * struct kapi_param_spec - Parameter specification\n+ * @name: Parameter name\n+ * @type_name: Type name as string\n+ * @type: Parameter type classification\n+ * @flags: Parameter attribute flags\n+ * @size: Size in bytes (for arrays/buffers)\n+ * @alignment: Required alignment\n+ * @min_value: Minimum valid value (for numeric types)\n+ * @max_value: Maximum valid value (for numeric types)\n+ * @valid_mask: Valid bits mask (for flag parameters)\n+ * @enum_values: Array of valid enumerated values\n+ * @enum_count: Number of valid enumerated values\n+ * @constraint_type: Type of constraint applied\n+ * @validate: Custom validation function\n+ * @description: Human-readable description\n+ * @constraints: Additional constraints description\n+ * @size_param_idx: 1-based index of the parameter that determines size,\n+ *                  or 0 if this parameter has a fixed size\n+ * @size_multiplier: Multiplier for size calculation (e.g., sizeof(struct))\n+ */\n+struct kapi_param_spec {\n+\tconst char *name;\n+\tconst char *type_name;\n+\tenum kapi_param_type type;\n+\tu32 flags;\n+\tsize_t size;\n+\tsize_t alignment;\n+\ts64 min_value;\n+\ts64 max_value;\n+\tu64 valid_mask;\n+\tconst s64 *enum_values;\n+\tu32 enum_count;\n+\tenum kapi_constraint_type constraint_type;\n+\tbool (*validate)(s64 value);\n+\tconst char *description;\n+\tconst char *constraints;\n+\tint size_param_idx;\t/* 1-based param index for dynamic size; 0 if N/A */\n+\tsize_t size_multiplier;\t/* Size per unit (e.g., sizeof(struct epoll_event)) */\n+};\n+\n+/**\n+ * struct kapi_error_spec - Error condition specification\n+ * @error_code: Error code value\n+ * @name: Error code name (e.g., \"EINVAL\")\n+ * @condition: Condition that triggers this error\n+ * @description: Detailed error description\n+ */\n+struct kapi_error_spec {\n+\tint error_code;\n+\tconst char *name;\n+\tconst char *condition;\n+\tconst char *description;\n+};\n+\n+/**\n+ * enum kapi_return_check_type - Return value check types\n+ * @KAPI_RETURN_EXACT: Success is an exact value\n+ * @KAPI_RETURN_RANGE: Success is within a range\n+ * @KAPI_RETURN_ERROR_CHECK: Success is when NOT in error list\n+ * @KAPI_RETURN_FD: Return value is a file descriptor (\u003e= 0 is success)\n+ * @KAPI_RETURN_CUSTOM: Custom validation function\n+ * @KAPI_RETURN_NO_RETURN: Function does not return (e.g., exec on success)\n+ */\n+enum kapi_return_check_type {\n+\tKAPI_RETURN_EXACT,\n+\tKAPI_RETURN_RANGE,\n+\tKAPI_RETURN_ERROR_CHECK,\n+\tKAPI_RETURN_FD,\n+\tKAPI_RETURN_CUSTOM,\n+\tKAPI_RETURN_NO_RETURN,\n+};\n+\n+/**\n+ * struct kapi_return_spec - Return value specification\n+ * @type_name: Return type name\n+ * @type: Return type classification\n+ * @check_type: Type of success check to perform\n+ * @success_value: Exact value indicating success (for EXACT)\n+ * @success_min: Minimum success value (for RANGE)\n+ * @success_max: Maximum success value (for RANGE)\n+ * @error_values: Array of error values (for ERROR_CHECK)\n+ * @error_count: Number of error values\n+ * @is_success: Custom function to check success\n+ * @description: Return value description\n+ */\n+struct kapi_return_spec {\n+\tconst char *type_name;\n+\tenum kapi_param_type type;\n+\tenum kapi_return_check_type check_type;\n+\ts64 success_value;\n+\ts64 success_min;\n+\ts64 success_max;\n+\tconst s64 *error_values;\n+\tu32 error_count;\n+\tbool (*is_success)(s64 retval);\n+\tconst char *description;\n+};\n+\n+/**\n+ * enum kapi_lock_scope - Lock acquisition/release scope\n+ * @KAPI_LOCK_INTERNAL: Lock is acquired and released within the function (common case)\n+ * @KAPI_LOCK_ACQUIRES: Function acquires lock but does not release it\n+ * @KAPI_LOCK_RELEASES: Function releases lock (must be held on entry)\n+ * @KAPI_LOCK_CALLER_HELD: Lock must be held by caller throughout the call\n+ */\n+enum kapi_lock_scope {\n+\tKAPI_LOCK_INTERNAL = 0,\n+\tKAPI_LOCK_ACQUIRES,\n+\tKAPI_LOCK_RELEASES,\n+\tKAPI_LOCK_CALLER_HELD,\n+};\n+\n+/**\n+ * struct kapi_lock_spec - Lock requirement specification\n+ * @lock_name: Name of the lock\n+ * @lock_type: Type of lock\n+ * @scope: Lock scope (internal, acquires, releases, or caller-held)\n+ * @description: Additional lock requirements\n+ */\n+struct kapi_lock_spec {\n+\tconst char *lock_name;\n+\tenum kapi_lock_type lock_type;\n+\tenum kapi_lock_scope scope;\n+\tconst char *description;\n+};\n+\n+/**\n+ * struct kapi_constraint_spec - Additional constraint specification\n+ * @name: Constraint name\n+ * @description: Constraint description\n+ * @expression: Formal expression (if applicable)\n+ */\n+struct kapi_constraint_spec {\n+\tconst char *name;\n+\tconst char *description;\n+\tconst char *expression;\n+};\n+\n+/**\n+ * enum kapi_signal_direction - Signal flow direction\n+ * @KAPI_SIGNAL_RECEIVE: Function may receive this signal\n+ * @KAPI_SIGNAL_SEND: Function may send this signal\n+ * @KAPI_SIGNAL_HANDLE: Function handles this signal specially\n+ * @KAPI_SIGNAL_BLOCK: Function blocks this signal\n+ * @KAPI_SIGNAL_IGNORE: Function ignores this signal\n+ */\n+enum kapi_signal_direction {\n+\tKAPI_SIGNAL_RECEIVE\t= (1 \u003c\u003c 0),\n+\tKAPI_SIGNAL_SEND\t= (1 \u003c\u003c 1),\n+\tKAPI_SIGNAL_HANDLE\t= (1 \u003c\u003c 2),\n+\tKAPI_SIGNAL_BLOCK\t= (1 \u003c\u003c 3),\n+\tKAPI_SIGNAL_IGNORE\t= (1 \u003c\u003c 4),\n+};\n+\n+/**\n+ * enum kapi_signal_action - What the function does with the signal\n+ * @KAPI_SIGNAL_ACTION_DEFAULT: Default signal action applies\n+ * @KAPI_SIGNAL_ACTION_TERMINATE: Causes termination\n+ * @KAPI_SIGNAL_ACTION_COREDUMP: Causes termination with core dump\n+ * @KAPI_SIGNAL_ACTION_STOP: Stops the process\n+ * @KAPI_SIGNAL_ACTION_CONTINUE: Continues a stopped process\n+ * @KAPI_SIGNAL_ACTION_CUSTOM: Custom handling described in notes\n+ * @KAPI_SIGNAL_ACTION_RETURN: Returns from syscall with EINTR\n+ * @KAPI_SIGNAL_ACTION_RESTART: Restarts the syscall\n+ * @KAPI_SIGNAL_ACTION_QUEUE: Queues the signal for later delivery\n+ * @KAPI_SIGNAL_ACTION_DISCARD: Discards the signal\n+ * @KAPI_SIGNAL_ACTION_TRANSFORM: Transforms to another signal\n+ */\n+enum kapi_signal_action {\n+\tKAPI_SIGNAL_ACTION_DEFAULT = 0,\n+\tKAPI_SIGNAL_ACTION_TERMINATE,\n+\tKAPI_SIGNAL_ACTION_COREDUMP,\n+\tKAPI_SIGNAL_ACTION_STOP,\n+\tKAPI_SIGNAL_ACTION_CONTINUE,\n+\tKAPI_SIGNAL_ACTION_CUSTOM,\n+\tKAPI_SIGNAL_ACTION_RETURN,\n+\tKAPI_SIGNAL_ACTION_RESTART,\n+\tKAPI_SIGNAL_ACTION_QUEUE,\n+\tKAPI_SIGNAL_ACTION_DISCARD,\n+\tKAPI_SIGNAL_ACTION_TRANSFORM,\n+};\n+\n+/**\n+ * struct kapi_signal_spec - Signal specification\n+ * @signal_num: Signal number (e.g., SIGKILL, SIGTERM)\n+ * @signal_name: Signal name as string\n+ * @direction: Direction flags (OR of kapi_signal_direction)\n+ * @action: What happens when signal is received\n+ * @target: Description of target process/thread for sent signals\n+ * @condition: Condition under which signal is sent/received/handled\n+ * @description: Detailed description of signal handling\n+ * @restartable: Whether syscall is restartable after this signal\n+ * @sa_flags_required: Required signal action flags (SA_*)\n+ * @sa_flags_forbidden: Forbidden signal action flags\n+ * @error_on_signal: Error code returned when signal occurs (-EINTR, etc)\n+ * @transform_to: Signal number to transform to (if action is TRANSFORM)\n+ * @timing: When signal can occur (\"entry\", \"during\", \"exit\", \"anytime\")\n+ * @priority: Signal handling priority (lower processed first)\n+ * @interruptible: Whether this operation is interruptible by this signal\n+ * @queue_behavior: How signal is queued (\"realtime\", \"standard\", \"coalesce\")\n+ * @state_required: Required process state for signal to be delivered\n+ * @state_forbidden: Forbidden process state for signal delivery\n+ */\n+struct kapi_signal_spec {\n+\tint signal_num;\n+\tconst char *signal_name;\n+\tu32 direction;\n+\tenum kapi_signal_action action;\n+\tconst char *target;\n+\tconst char *condition;\n+\tconst char *description;\n+\tbool restartable;\n+\tu32 sa_flags_required;\n+\tu32 sa_flags_forbidden;\n+\tint error_on_signal;\n+\tint transform_to;\n+\tconst char *timing;\n+\tu8 priority;\n+\tbool interruptible;\n+\tconst char *queue_behavior;\n+\tu32 state_required;\n+\tu32 state_forbidden;\n+};\n+\n+/**\n+ * struct kapi_signal_mask_spec - Signal mask specification\n+ * @mask_name: Name of the signal mask\n+ * @signals: Array of signal numbers in the mask\n+ * @signal_count: Number of signals in the mask\n+ * @description: Description of what this mask represents\n+ */\n+struct kapi_signal_mask_spec {\n+\tconst char *mask_name;\n+\tint signals[KAPI_MAX_SIGNALS];\n+\tu32 signal_count;\n+\tconst char *description;\n+};\n+\n+/**\n+ * struct kapi_struct_field - Structure field specification\n+ * @name: Field name\n+ * @type: Field type classification\n+ * @type_name: Type name as string\n+ * @offset: Offset within structure\n+ * @size: Size of field in bytes\n+ * @flags: Field attribute flags\n+ * @constraint_type: Type of constraint applied\n+ * @min_value: Minimum valid value (for numeric types)\n+ * @max_value: Maximum valid value (for numeric types)\n+ * @valid_mask: Valid bits mask (for flag fields)\n+ * @enum_values: Comma-separated list of valid enum values (for enum types)\n+ * @description: Field description\n+ */\n+struct kapi_struct_field {\n+\tconst char *name;\n+\tenum kapi_param_type type;\n+\tconst char *type_name;\n+\tsize_t offset;\n+\tsize_t size;\n+\tu32 flags;\n+\tenum kapi_constraint_type constraint_type;\n+\ts64 min_value;\n+\ts64 max_value;\n+\tu64 valid_mask;\n+\tconst char *enum_values;\t/* Comma-separated list of valid enum values */\n+\tconst char *description;\n+};\n+\n+/**\n+ * struct kapi_struct_spec - Structure type specification\n+ * @name: Structure name\n+ * @size: Total size of structure\n+ * @alignment: Required alignment\n+ * @field_count: Number of fields\n+ * @fields: Field specifications\n+ * @description: Structure description\n+ */\n+struct kapi_struct_spec {\n+\tconst char *name;\n+\tsize_t size;\n+\tsize_t alignment;\n+\tu32 field_count;\n+\tstruct kapi_struct_field fields[KAPI_MAX_PARAMS];\n+\tconst char *description;\n+};\n+\n+/**\n+ * enum kapi_capability_action - What the capability allows\n+ * @KAPI_CAP_BYPASS_CHECK: Bypasses a check entirely\n+ * @KAPI_CAP_INCREASE_LIMIT: Increases or removes a limit\n+ * @KAPI_CAP_OVERRIDE_RESTRICTION: Overrides a restriction\n+ * @KAPI_CAP_GRANT_PERMISSION: Grants permission that would otherwise be denied\n+ * @KAPI_CAP_MODIFY_BEHAVIOR: Changes the behavior of the operation\n+ * @KAPI_CAP_ACCESS_RESOURCE: Allows access to restricted resources\n+ * @KAPI_CAP_PERFORM_OPERATION: Allows performing a privileged operation\n+ */\n+enum kapi_capability_action {\n+\tKAPI_CAP_BYPASS_CHECK = 0,\n+\tKAPI_CAP_INCREASE_LIMIT,\n+\tKAPI_CAP_OVERRIDE_RESTRICTION,\n+\tKAPI_CAP_GRANT_PERMISSION,\n+\tKAPI_CAP_MODIFY_BEHAVIOR,\n+\tKAPI_CAP_ACCESS_RESOURCE,\n+\tKAPI_CAP_PERFORM_OPERATION,\n+};\n+\n+/**\n+ * struct kapi_capability_spec - Capability requirement specification\n+ * @capability: The capability constant (e.g., CAP_IPC_LOCK)\n+ * @cap_name: Capability name as string\n+ * @action: What the capability allows (kapi_capability_action)\n+ * @allows: Description of what the capability allows\n+ * @without_cap: What happens without the capability\n+ * @check_condition: Condition when capability is checked\n+ * @priority: Check priority (lower checked first)\n+ * @alternative: Alternative capabilities that can be used\n+ * @alternative_count: Number of alternative capabilities\n+ */\n+struct kapi_capability_spec {\n+\tint capability;\n+\tconst char *cap_name;\n+\tenum kapi_capability_action action;\n+\tconst char *allows;\n+\tconst char *without_cap;\n+\tconst char *check_condition;\n+\tu8 priority;\n+\tint alternative[KAPI_MAX_CAPABILITIES];\n+\tu32 alternative_count;\n+};\n+\n+/**\n+ * enum kapi_side_effect_type - Types of side effects\n+ * @KAPI_EFFECT_NONE: No side effects\n+ * @KAPI_EFFECT_ALLOC_MEMORY: Allocates memory\n+ * @KAPI_EFFECT_FREE_MEMORY: Frees memory\n+ * @KAPI_EFFECT_MODIFY_STATE: Modifies global/shared state\n+ * @KAPI_EFFECT_SIGNAL_SEND: Sends signals\n+ * @KAPI_EFFECT_FILE_POSITION: Modifies file position\n+ * @KAPI_EFFECT_LOCK_ACQUIRE: Acquires locks\n+ * @KAPI_EFFECT_LOCK_RELEASE: Releases locks\n+ * @KAPI_EFFECT_RESOURCE_CREATE: Creates system resources (FDs, PIDs, etc)\n+ * @KAPI_EFFECT_RESOURCE_DESTROY: Destroys system resources\n+ * @KAPI_EFFECT_SCHEDULE: May cause scheduling/context switch\n+ * @KAPI_EFFECT_HARDWARE: Interacts with hardware\n+ * @KAPI_EFFECT_NETWORK: Network I/O operation\n+ * @KAPI_EFFECT_FILESYSTEM: Filesystem modification\n+ * @KAPI_EFFECT_PROCESS_STATE: Modifies process state\n+ * @KAPI_EFFECT_IRREVERSIBLE: Effect cannot be undone\n+ */\n+enum kapi_side_effect_type {\n+\tKAPI_EFFECT_NONE = 0,\n+\tKAPI_EFFECT_ALLOC_MEMORY = (1 \u003c\u003c 0),\n+\tKAPI_EFFECT_FREE_MEMORY = (1 \u003c\u003c 1),\n+\tKAPI_EFFECT_MODIFY_STATE = (1 \u003c\u003c 2),\n+\tKAPI_EFFECT_SIGNAL_SEND = (1 \u003c\u003c 3),\n+\tKAPI_EFFECT_FILE_POSITION = (1 \u003c\u003c 4),\n+\tKAPI_EFFECT_LOCK_ACQUIRE = (1 \u003c\u003c 5),\n+\tKAPI_EFFECT_LOCK_RELEASE = (1 \u003c\u003c 6),\n+\tKAPI_EFFECT_RESOURCE_CREATE = (1 \u003c\u003c 7),\n+\tKAPI_EFFECT_RESOURCE_DESTROY = (1 \u003c\u003c 8),\n+\tKAPI_EFFECT_SCHEDULE = (1 \u003c\u003c 9),\n+\tKAPI_EFFECT_HARDWARE = (1 \u003c\u003c 10),\n+\tKAPI_EFFECT_NETWORK = (1 \u003c\u003c 11),\n+\tKAPI_EFFECT_FILESYSTEM = (1 \u003c\u003c 12),\n+\tKAPI_EFFECT_PROCESS_STATE = (1 \u003c\u003c 13),\n+\tKAPI_EFFECT_IRREVERSIBLE = (1 \u003c\u003c 14),\n+};\n+\n+/**\n+ * struct kapi_side_effect - Side effect specification\n+ * @type: Bitmask of effect types\n+ * @target: What is affected (e.g., \"process memory\", \"file descriptor table\")\n+ * @condition: Condition under which effect occurs\n+ * @description: Detailed description of the effect\n+ * @reversible: Whether the effect can be undone\n+ */\n+struct kapi_side_effect {\n+\tu32 type;\n+\tconst char *target;\n+\tconst char *condition;\n+\tconst char *description;\n+\tbool reversible;\n+};\n+\n+/**\n+ * struct kapi_state_transition - State transition specification\n+ * @from_state: Starting state description\n+ * @to_state: Ending state description\n+ * @condition: Condition for transition\n+ * @object: Object whose state changes\n+ * @description: Detailed description\n+ */\n+struct kapi_state_transition {\n+\tconst char *from_state;\n+\tconst char *to_state;\n+\tconst char *condition;\n+\tconst char *object;\n+\tconst char *description;\n+};\n+\n+#define KAPI_MAX_STRUCT_SPECS\t8\n+#define KAPI_MAX_SIDE_EFFECTS\t32\n+#define KAPI_MAX_STATE_TRANS\t8\n+\n+/**\n+ * struct kernel_api_spec - Complete kernel API specification\n+ * @name: Function name\n+ * @version: API version\n+ * @description: Brief description\n+ * @long_description: Detailed description\n+ * @context_flags: Execution context flags\n+ * @param_count: Number of parameters\n+ * @params: Parameter specifications\n+ * @return_spec: Return value specification\n+ * @error_count: Number of possible errors\n+ * @errors: Error specifications\n+ * @lock_count: Number of lock specifications\n+ * @locks: Lock requirement specifications\n+ * @constraint_count: Number of additional constraints\n+ * @constraints: Additional constraint specifications\n+ * @examples: Usage examples\n+ * @notes: Additional notes\n+ * @signal_count: Number of signal specifications\n+ * @signals: Signal handling specifications\n+ * @signal_mask_count: Number of signal mask specifications\n+ * @signal_masks: Signal mask specifications\n+ * @struct_spec_count: Number of structure specifications\n+ * @struct_specs: Structure type specifications\n+ * @side_effect_count: Number of side effect specifications\n+ * @side_effects: Side effect specifications\n+ * @state_trans_count: Number of state transition specifications\n+ * @state_transitions: State transition specifications\n+ * @capability_count: Number of required capabilities\n+ * @capabilities: Required capability specifications\n+ * @param_magic: Magic value marking the start of the params array\n+ * @return_magic: Magic value marking the return spec\n+ * @error_magic: Magic value marking the start of the errors array\n+ * @lock_magic: Magic value marking the start of the locks array\n+ * @constraint_magic: Magic value marking the constraints array\n+ * @info_magic: Magic value marking the info block (examples, notes)\n+ * @signal_magic: Magic value marking the start of the signals array\n+ * @sigmask_magic: Magic value marking the signal masks array\n+ * @struct_magic: Magic value marking the struct specs array\n+ * @effect_magic: Magic value marking the side effects array\n+ * @trans_magic: Magic value marking the state transitions array\n+ * @cap_magic: Magic value marking the capabilities array\n+ */\n+struct kernel_api_spec {\n+\tconst char *name;\n+\tu32 version;\n+\tconst char *description;\n+\tconst char *long_description;\n+\tu32 context_flags;\n+\n+\t/* Parameters */\n+\tu32 param_magic;  /* 0x4B415031 = 'KAP1' */\n+\tu32 param_count;\n+\tstruct kapi_param_spec params[KAPI_MAX_PARAMS];\n+\n+\t/* Return value */\n+\tu32 return_magic; /* 0x4B415232 = 'KAR2' */\n+\tstruct kapi_return_spec return_spec;\n+\n+\t/* Errors */\n+\tu32 error_magic;  /* 0x4B414533 = 'KAE3' */\n+\tu32 error_count;\n+\tstruct kapi_error_spec errors[KAPI_MAX_ERRORS];\n+\n+\t/* Locking */\n+\tu32 lock_magic;   /* 0x4B414C34 = 'KAL4' */\n+\tu32 lock_count;\n+\tstruct kapi_lock_spec locks[KAPI_MAX_LOCKS];\n+\n+\t/* Constraints */\n+\tu32 constraint_magic; /* 0x4B414335 = 'KAC5' */\n+\tu32 constraint_count;\n+\tstruct kapi_constraint_spec constraints[KAPI_MAX_CONSTRAINTS];\n+\n+\t/* Additional information */\n+\tu32 info_magic;   /* 0x4B414936 = 'KAI6' */\n+\tconst char *examples;\n+\tconst char *notes;\n+\n+\t/* Signal specifications */\n+\tu32 signal_magic; /* 0x4B415337 = 'KAS7' */\n+\tu32 signal_count;\n+\tstruct kapi_signal_spec signals[KAPI_MAX_SIGNALS];\n+\n+\t/* Signal mask specifications */\n+\tu32 sigmask_magic; /* 0x4B414D38 = 'KAM8' */\n+\tu32 signal_mask_count;\n+\tstruct kapi_signal_mask_spec signal_masks[KAPI_MAX_SIGNALS];\n+\n+\t/* Structure specifications */\n+\tu32 struct_magic; /* 0x4B415439 = 'KAT9' */\n+\tu32 struct_spec_count;\n+\tstruct kapi_struct_spec struct_specs[KAPI_MAX_STRUCT_SPECS];\n+\n+\t/* Side effects */\n+\tu32 effect_magic; /* 0x4B414641 = 'KAFA' */\n+\tu32 side_effect_count;\n+\tstruct kapi_side_effect side_effects[KAPI_MAX_SIDE_EFFECTS];\n+\n+\t/* State transitions */\n+\tu32 trans_magic;  /* 0x4B415442 = 'KATB' */\n+\tu32 state_trans_count;\n+\tstruct kapi_state_transition state_transitions[KAPI_MAX_STATE_TRANS];\n+\n+\t/* Capability specifications */\n+\tu32 cap_magic;    /* 0x4B414343 = 'KACC' */\n+\tu32 capability_count;\n+\tstruct kapi_capability_spec capabilities[KAPI_MAX_CAPABILITIES];\n+};\n+\n+/* Macros for defining API specifications */\n+\n+/**\n+ * DEFINE_KERNEL_API_SPEC - Define a kernel API specification\n+ * @func_name: Function name to specify\n+ *\n+ * The ``.kapi_specs`` section holds an array of pointers to\n+ * fully-defined ``kernel_api_spec`` instances, tightly packed so\n+ * iteration ``for (pp = __start_kapi_specs; pp \u003c __stop_kapi_specs;\n+ * pp++)`` advances by one pointer each step regardless of the real\n+ * spec struct size.\n+ */\n+#define DEFINE_KERNEL_API_SPEC(func_name)\t\t\t\t\t\\\n+\textern const struct kernel_api_spec __kapi_spec_##func_name;\t\t\\\n+\tstatic const struct kernel_api_spec * const\t\t\t\t\\\n+\t__kapi_spec_ptr_##func_name __used __section(\".kapi_specs\") =\t\t\\\n+\t\t\u0026__kapi_spec_##func_name;\t\t\t\t\t\\\n+\tconst struct kernel_api_spec __kapi_spec_##func_name = {\t\t\\\n+\t\t.name = __stringify(func_name),\t\t\t\t\t\\\n+\t\t.version = 1,\n+\n+/**\n+ * KAPI_DESCRIPTION - Set API description\n+ * @desc: Description string\n+ */\n+#define KAPI_DESCRIPTION(desc) \\\n+\t.description = desc,\n+\n+/**\n+ * KAPI_LONG_DESC - Set detailed API description\n+ * @desc: Detailed description string\n+ */\n+#define KAPI_LONG_DESC(desc) \\\n+\t.long_description = desc,\n+\n+/**\n+ * KAPI_CONTEXT - Set execution context flags\n+ * @flags: Context flags (OR'ed KAPI_CTX_* values)\n+ */\n+#define KAPI_CONTEXT(flags) \\\n+\t.context_flags = flags,\n+\n+/**\n+ * KAPI_PARAM - Define a parameter specification\n+ * @idx: Parameter index (0-based)\n+ * @pname: Parameter name\n+ * @ptype: Type name string\n+ * @pdesc: Parameter description\n+ */\n+#define KAPI_PARAM(idx, pname, ptype, pdesc) \\\n+\t.params[idx] = {\t\t\t\\\n+\t\t.name = pname,\t\t\t\\\n+\t\t.type_name = ptype,\t\t\\\n+\t\t.description = pdesc,\n+\n+#define KAPI_PARAM_TYPE(ptype) \\\n+\t\t.type = ptype,\n+\n+#define KAPI_PARAM_FLAGS(pflags) \\\n+\t\t.flags = pflags,\n+\n+#define KAPI_PARAM_SIZE(psize) \\\n+\t\t.size = psize,\n+\n+#define KAPI_PARAM_RANGE(pmin, pmax) \\\n+\t\t.min_value = pmin,\t\\\n+\t\t.max_value = pmax,\n+\n+#define KAPI_PARAM_CONSTRAINT_TYPE(ctype) \\\n+\t\t.constraint_type = ctype,\n+\n+#define KAPI_PARAM_CONSTRAINT(desc) \\\n+\t\t.constraints = desc,\n+\n+#define KAPI_PARAM_VALID_MASK(mask) \\\n+\t\t.valid_mask = mask,\n+\n+/**\n+ * KAPI_PARAM_ENUM_VALUES - Set the valid values of an enumerated parameter\n+ * @...: Variadic list of valid values\n+ */\n+#define KAPI_PARAM_ENUM_VALUES(...) \\\n+\t\t.enum_values = (const s64[]){ __VA_ARGS__ }, \\\n+\t\t.enum_count = sizeof((const s64[]){ __VA_ARGS__ }) / sizeof(s64),\n+\n+#define KAPI_PARAM_ALIGNMENT(align) \\\n+\t\t.alignment = align,\n+\n+/*\n+ * Store the 1-based parameter index so the zero-initialised default\n+ * (no dynamic sizing) remains distinguishable from \"uses param 0\".\n+ */\n+#define KAPI_PARAM_SIZE_PARAM(idx) \\\n+\t\t.size_param_idx = (idx) + 1,\n+\n+/**\n+ * KAPI_PARAM_COUNT - Set the number of parameters\n+ * @n: Number of parameters\n+ */\n+#define KAPI_PARAM_COUNT(n) \\\n+\t.param_magic = KAPI_MAGIC_PARAMS, \\\n+\t.param_count = n,\n+\n+/**\n+ * KAPI_RETURN - Define return value specification\n+ * @rtype: Return type name\n+ * @rdesc: Return value description\n+ */\n+#define KAPI_RETURN(rtype, rdesc) \\\n+\t.return_magic = KAPI_MAGIC_RETURN, \\\n+\t.return_spec = {\t\t\\\n+\t\t.type_name = rtype,\t\\\n+\t\t.description = rdesc,\n+\n+#define KAPI_RETURN_SUCCESS(val, ...) \\\n+\t\t.success_value = val,\n+\n+#define KAPI_RETURN_TYPE(rtype) \\\n+\t\t.type = rtype,\n+\n+#define KAPI_RETURN_CHECK_TYPE(ctype) \\\n+\t\t.check_type = ctype,\n+\n+#define KAPI_RETURN_ERROR_VALUES(values) \\\n+\t\t.error_values = values,\n+\n+#define KAPI_RETURN_ERROR_COUNT(count) \\\n+\t\t.error_count = count,\n+\n+#define KAPI_RETURN_SUCCESS_RANGE(min, max) \\\n+\t\t.success_min = min, \\\n+\t\t.success_max = max,\n+\n+/**\n+ * KAPI_ERROR - Define an error condition\n+ * @idx: Error index\n+ * @ecode: Error code value\n+ * @ename: Error name\n+ * @econd: Error condition\n+ * @edesc: Error description\n+ */\n+#define KAPI_ERROR(idx, ecode, ename, econd, edesc) \\\n+\t.errors[idx] = {\t\t\t\\\n+\t\t.error_code = ecode,\t\t\\\n+\t\t.name = ename,\t\t\t\\\n+\t\t.condition = econd,\t\t\\\n+\t\t.description = edesc,\t\t\\\n+\t},\n+\n+/**\n+ * KAPI_ERROR_COUNT - Set the number of errors\n+ * @n: Number of errors\n+ */\n+#define KAPI_ERROR_COUNT(n) \\\n+\t.error_magic = KAPI_MAGIC_ERRORS, \\\n+\t.error_count = n,\n+\n+/**\n+ * KAPI_LOCK - Define a lock requirement\n+ * @idx: Lock index\n+ * @lname: Lock name\n+ * @ltype: Lock type\n+ */\n+#define KAPI_LOCK(idx, lname, ltype) \\\n+\t.locks[idx] = {\t\t\t\\\n+\t\t.lock_name = lname,\t\\\n+\t\t.lock_type = ltype,\n+\n+#define KAPI_LOCK_ACQUIRED \\\n+\t\t.scope = KAPI_LOCK_ACQUIRES,\n+\n+#define KAPI_LOCK_RELEASED \\\n+\t\t.scope = KAPI_LOCK_RELEASES,\n+\n+#define KAPI_LOCK_HELD_ENTRY \\\n+\t\t.scope = KAPI_LOCK_CALLER_HELD,\n+\n+#define KAPI_LOCK_HELD_EXIT \\\n+\t\t.scope = KAPI_LOCK_CALLER_HELD,\n+\n+#define KAPI_LOCK_DESC(ldesc) \\\n+\t\t.description = ldesc,\n+\n+/**\n+ * KAPI_CONSTRAINT - Define an additional constraint\n+ * @idx: Constraint index\n+ * @cname: Constraint name\n+ * @cdesc: Constraint description\n+ */\n+#define KAPI_CONSTRAINT(idx, cname, cdesc) \\\n+\t.constraints[idx] = {\t\t\\\n+\t\t.name = cname,\t\t\\\n+\t\t.description = cdesc,\n+\n+#define KAPI_CONSTRAINT_EXPR(expr) \\\n+\t\t.expression = expr,\n+\n+/**\n+ * KAPI_EXAMPLES - Set API usage examples\n+ * @ex: Examples string\n+ */\n+#define KAPI_EXAMPLES(ex) \\\n+\t.info_magic = KAPI_MAGIC_INFO, \\\n+\t.examples = ex,\n+\n+/**\n+ * KAPI_NOTES - Set API notes\n+ * @n: Notes string\n+ */\n+#define KAPI_NOTES(n) \\\n+\t.notes = n,\n+\n+\n+/**\n+ * KAPI_SIGNAL - Define a signal specification\n+ * @idx: Signal index\n+ * @signum: Signal number (e.g., SIGKILL)\n+ * @signame: Signal name string\n+ * @dir: Direction flags\n+ * @act: Action taken\n+ */\n+#define KAPI_SIGNAL(idx, signum, signame, dir, act) \\\n+\t.signals[idx] = {\t\t\t\\\n+\t\t.signal_num = signum,\t\t\\\n+\t\t.signal_name = signame,\t\t\\\n+\t\t.direction = dir,\t\t\\\n+\t\t.action = act,\n+\n+#define KAPI_SIGNAL_TARGET(tgt) \\\n+\t\t.target = tgt,\n+\n+#define KAPI_SIGNAL_CONDITION(cond) \\\n+\t\t.condition = cond,\n+\n+#define KAPI_SIGNAL_DESC(desc) \\\n+\t\t.description = desc,\n+\n+#define KAPI_SIGNAL_RESTARTABLE \\\n+\t\t.restartable = true,\n+\n+#define KAPI_SIGNAL_SA_FLAGS_REQ(flags) \\\n+\t\t.sa_flags_required = flags,\n+\n+#define KAPI_SIGNAL_SA_FLAGS_FORBID(flags) \\\n+\t\t.sa_flags_forbidden = flags,\n+\n+#define KAPI_SIGNAL_ERROR(err) \\\n+\t\t.error_on_signal = err,\n+\n+#define KAPI_SIGNAL_TRANSFORM(sig) \\\n+\t\t.transform_to = sig,\n+\n+#define KAPI_SIGNAL_TIMING(when) \\\n+\t\t.timing = when,\n+\n+#define KAPI_SIGNAL_PRIORITY(prio) \\\n+\t\t.priority = prio,\n+\n+#define KAPI_SIGNAL_INTERRUPTIBLE \\\n+\t\t.interruptible = true,\n+\n+#define KAPI_SIGNAL_QUEUE(behavior) \\\n+\t\t.queue_behavior = behavior,\n+\n+#define KAPI_SIGNAL_STATE_REQ(state) \\\n+\t\t.state_required = state,\n+\n+#define KAPI_SIGNAL_STATE_FORBID(state) \\\n+\t\t.state_forbidden = state,\n+\n+#define KAPI_SIGNAL_COUNT(n) \\\n+\t.signal_magic = KAPI_MAGIC_SIGNALS, \\\n+\t.signal_count = n,\n+\n+/**\n+ * KAPI_SIGNAL_MASK - Define a signal mask specification\n+ * @idx: Mask index\n+ * @name: Mask name\n+ * @desc: Mask description\n+ */\n+#define KAPI_SIGNAL_MASK(idx, name, desc) \\\n+\t.signal_masks[idx] = {\t\t\\\n+\t\t.mask_name = name,\t\\\n+\t\t.description = desc,\n+\n+/*\n+ * KAPI_SIGNAL_MASK_SIGNALS - Specify signals in a signal mask\n+ * @...: Variadic list of signal numbers\n+ *\n+ * Usage:\n+ *   KAPI_SIGNAL_MASK(0, \"blocked\", \"Signals blocked during operation\")\n+ *   KAPI_SIGNAL_MASK_SIGNALS(SIGINT, SIGTERM, SIGQUIT)\n+ *   },\n+ */\n+#define KAPI_SIGNAL_MASK_SIGNALS(...) \\\n+\t\t.signals = { __VA_ARGS__ }, \\\n+\t\t.signal_count = sizeof((int[]){ __VA_ARGS__ }) / sizeof(int),\n+\n+/**\n+ * KAPI_SIGNAL_MASK_COUNT - Set the number of signal mask specifications\n+ * @n: Number of signal masks\n+ */\n+#define KAPI_SIGNAL_MASK_COUNT(n) \\\n+\t.sigmask_magic = KAPI_MAGIC_SIGMASK, \\\n+\t.signal_mask_count = n,\n+\n+/**\n+ * KAPI_STRUCT_SPEC - Define a structure specification\n+ * @idx: Structure spec index\n+ * @sname: Structure name\n+ * @sdesc: Structure description\n+ */\n+#define KAPI_STRUCT_SPEC(idx, sname, sdesc) \\\n+\t.struct_specs[idx] = {\t\t\\\n+\t\t.name = #sname,\t\t\\\n+\t\t.description = sdesc,\n+\n+#define KAPI_STRUCT_SIZE(ssize, salign) \\\n+\t\t.size = ssize,\t\t\\\n+\t\t.alignment = salign,\n+\n+#define KAPI_STRUCT_FIELD_COUNT(n) \\\n+\t\t.field_count = n,\n+\n+/**\n+ * KAPI_STRUCT_FIELD - Define a structure field\n+ * @fidx: Field index\n+ * @fname: Field name\n+ * @ftype: Field type (KAPI_TYPE_*)\n+ * @ftype_name: Type name as string\n+ * @fdesc: Field description\n+ */\n+#define KAPI_STRUCT_FIELD(fidx, fname, ftype, ftype_name, fdesc) \\\n+\t\t.fields[fidx] = {\t\\\n+\t\t\t.name = fname,\t\\\n+\t\t\t.type = ftype,\t\\\n+\t\t\t.type_name = ftype_name, \\\n+\t\t\t.description = fdesc,\n+\n+#define KAPI_FIELD_OFFSET(foffset) \\\n+\t\t\t.offset = foffset,\n+\n+#define KAPI_FIELD_SIZE(fsize) \\\n+\t\t\t.size = fsize,\n+\n+#define KAPI_FIELD_FLAGS(fflags) \\\n+\t\t\t.flags = fflags,\n+\n+#define KAPI_FIELD_CONSTRAINT_RANGE(min, max) \\\n+\t\t\t.constraint_type = KAPI_CONSTRAINT_RANGE, \\\n+\t\t\t.min_value = min, \\\n+\t\t\t.max_value = max,\n+\n+#define KAPI_FIELD_CONSTRAINT_MASK(mask) \\\n+\t\t\t.constraint_type = KAPI_CONSTRAINT_MASK, \\\n+\t\t\t.valid_mask = mask,\n+\n+#define KAPI_FIELD_CONSTRAINT_ENUM(values) \\\n+\t\t\t.constraint_type = KAPI_CONSTRAINT_ENUM, \\\n+\t\t\t.enum_values = values,\n+\n+/* Counter for structure specifications */\n+#define KAPI_STRUCT_SPEC_COUNT(n) \\\n+\t.struct_magic = KAPI_MAGIC_STRUCTS, \\\n+\t.struct_spec_count = n,\n+\n+/* Additional lock-related macros */\n+#define KAPI_LOCK_COUNT(n) \\\n+\t.lock_magic = KAPI_MAGIC_LOCKS, \\\n+\t.lock_count = n,\n+\n+/**\n+ * KAPI_SIDE_EFFECT - Define a side effect\n+ * @idx: Side effect index\n+ * @etype: Effect type bitmask (OR'ed KAPI_EFFECT_* values)\n+ * @etarget: What is affected\n+ * @edesc: Effect description\n+ */\n+#define KAPI_SIDE_EFFECT(idx, etype, etarget, edesc) \\\n+\t.side_effects[idx] = {\t\t\\\n+\t\t.type = etype,\t\t\\\n+\t\t.target = etarget,\t\\\n+\t\t.description = edesc,\n+\n+#define KAPI_EFFECT_CONDITION(cond) \\\n+\t\t.condition = cond,\n+\n+#define KAPI_EFFECT_REVERSIBLE \\\n+\t\t.reversible = true,\n+\n+/**\n+ * KAPI_STATE_TRANS - Define a state transition\n+ * @idx: State transition index\n+ * @obj: Object whose state changes\n+ * @from: From state\n+ * @to: To state\n+ * @desc: Transition description\n+ */\n+#define KAPI_STATE_TRANS(idx, obj, from, to, desc) \\\n+\t.state_transitions[idx] = {\t\\\n+\t\t.object = obj,\t\t\\\n+\t\t.from_state = from,\t\\\n+\t\t.to_state = to,\t\t\\\n+\t\t.description = desc,\n+\n+#define KAPI_STATE_TRANS_COND(cond) \\\n+\t\t.condition = cond,\n+\n+/* Counters for side effects and state transitions */\n+#define KAPI_SIDE_EFFECT_COUNT(n) \\\n+\t.effect_magic = KAPI_MAGIC_EFFECTS, \\\n+\t.side_effect_count = n,\n+\n+#define KAPI_STATE_TRANS_COUNT(n) \\\n+\t.trans_magic = KAPI_MAGIC_TRANS, \\\n+\t.state_trans_count = n,\n+\n+/* Helper macros for common side effect patterns */\n+#define KAPI_EFFECTS_MEMORY\t(KAPI_EFFECT_ALLOC_MEMORY | KAPI_EFFECT_FREE_MEMORY)\n+#define KAPI_EFFECTS_LOCKING\t(KAPI_EFFECT_LOCK_ACQUIRE | KAPI_EFFECT_LOCK_RELEASE)\n+#define KAPI_EFFECTS_RESOURCES\t(KAPI_EFFECT_RESOURCE_CREATE | KAPI_EFFECT_RESOURCE_DESTROY)\n+#define KAPI_EFFECTS_IO\t\t(KAPI_EFFECT_NETWORK | KAPI_EFFECT_FILESYSTEM)\n+\n+/* Common signal timing constants */\n+#define KAPI_SIGNAL_TIME_ENTRY\t\t\"entry\"\n+#define KAPI_SIGNAL_TIME_DURING\t\t\"during\"\n+#define KAPI_SIGNAL_TIME_EXIT\t\t\"exit\"\n+#define KAPI_SIGNAL_TIME_ANYTIME\t\"anytime\"\n+#define KAPI_SIGNAL_TIME_BLOCKING\t\"while_blocked\"\n+#define KAPI_SIGNAL_TIME_SLEEPING\t\"while_sleeping\"\n+#define KAPI_SIGNAL_TIME_BEFORE\t\t\"before\"\n+#define KAPI_SIGNAL_TIME_AFTER\t\t\"after\"\n+\n+/* Common signal queue behaviors */\n+#define KAPI_SIGNAL_QUEUE_STANDARD\t\"standard\"\n+#define KAPI_SIGNAL_QUEUE_REALTIME\t\"realtime\"\n+#define KAPI_SIGNAL_QUEUE_COALESCE\t\"coalesce\"\n+#define KAPI_SIGNAL_QUEUE_REPLACE\t\"replace\"\n+#define KAPI_SIGNAL_QUEUE_DISCARD\t\"discard\"\n+\n+/* Process state flags for signal delivery */\n+#define KAPI_SIGNAL_STATE_RUNNING\tBIT(0)\n+#define KAPI_SIGNAL_STATE_SLEEPING\tBIT(1)\n+#define KAPI_SIGNAL_STATE_STOPPED\tBIT(2)\n+#define KAPI_SIGNAL_STATE_TRACED\tBIT(3)\n+#define KAPI_SIGNAL_STATE_ZOMBIE\tBIT(4)\n+#define KAPI_SIGNAL_STATE_DEAD\t\tBIT(5)\n+\n+/* Capability specification macros */\n+\n+/**\n+ * KAPI_CAPABILITY - Define a capability requirement\n+ * @idx: Capability index\n+ * @cap: Capability constant (e.g., CAP_IPC_LOCK)\n+ * @name: Capability name string\n+ * @act: Action type (kapi_capability_action)\n+ */\n+#define KAPI_CAPABILITY(idx, cap, name, act) \\\n+\t.capabilities[idx] = {\t\t\\\n+\t\t.capability = cap,\t\\\n+\t\t.cap_name = name,\t\\\n+\t\t.action = act,\n+\n+#define KAPI_CAP_ALLOWS(desc) \\\n+\t\t.allows = desc,\n+\n+#define KAPI_CAP_WITHOUT(desc) \\\n+\t\t.without_cap = desc,\n+\n+#define KAPI_CAP_CONDITION(cond) \\\n+\t\t.check_condition = cond,\n+\n+#define KAPI_CAP_PRIORITY(prio) \\\n+\t\t.priority = prio,\n+\n+/*\n+ * KAPI_CAP_ALTERNATIVE - Capabilities that can be used instead\n+ * @...: Variadic list of capability numbers\n+ */\n+#define KAPI_CAP_ALTERNATIVE(...) \\\n+\t\t.alternative = { __VA_ARGS__ }, \\\n+\t\t.alternative_count = sizeof((int[]){ __VA_ARGS__ }) / sizeof(int),\n+\n+/* Counter for capability specifications */\n+#define KAPI_CAPABILITY_COUNT(n) \\\n+\t.cap_magic = KAPI_MAGIC_CAPS, \\\n+\t.capability_count = n,\n+\n+/* Validation and runtime checking */\n+\n+#ifdef CONFIG_KAPI_RUNTIME_CHECKS\n+bool kapi_validate_param(const struct kapi_param_spec *param_spec, s64 value);\n+bool kapi_validate_param_with_context(const struct kapi_param_spec *param_spec,\n+\t\t\t\t       s64 value, const s64 *all_params, int param_count);\n+bool kapi_check_return_success(const struct kapi_return_spec *return_spec, s64 retval);\n+bool kapi_validate_return_value(const struct kernel_api_spec *spec, s64 retval);\n+void kapi_check_context(const struct kernel_api_spec *spec);\n+#else\n+static inline bool kapi_validate_param(const struct kapi_param_spec *param_spec, s64 value)\n+{\n+\treturn true;\n+}\n+static inline bool\n+kapi_validate_param_with_context(const struct kapi_param_spec *param_spec,\n+\t\t\t\t s64 value, const s64 *all_params, int param_count)\n+{\n+\treturn true;\n+}\n+static inline bool kapi_check_return_success(const struct kapi_return_spec *return_spec, s64 retval)\n+{\n+\treturn true;\n+}\n+static inline bool kapi_validate_return_value(const struct kernel_api_spec *spec, s64 retval)\n+{\n+\treturn true;\n+}\n+static inline void kapi_check_context(const struct kernel_api_spec *spec) {}\n+#endif\n+\n+/* Export/query functions */\n+int kapi_export_json(const struct kernel_api_spec *spec, char *buf, size_t size);\n+\n+/* Registration for dynamic APIs */\n+int kapi_register_spec(const struct kernel_api_spec *spec);\n+void kapi_unregister_spec(const char *name);\n+\n+#define KAPI_CONSTRAINT_COUNT(n) \\\n+\t.constraint_magic = KAPI_MAGIC_CONSTRAINTS, \\\n+\t.constraint_count = n,\n+\n+#endif /* _LINUX_KERNEL_API_SPEC_H */\ndiff --git a/include/linux/syscalls.h b/include/linux/syscalls.h\nindex 8413b624ad478..b062bac9ea1ed 100644\n--- a/include/linux/syscalls.h\n+++ b/include/linux/syscalls.h\n@@ -94,6 +94,10 @@ struct file_attr;\n #include \u003clinux/personality.h\u003e\n #include \u003ctrace/syscall.h\u003e\n \n+#ifdef CONFIG_KAPI_RUNTIME_CHECKS\n+#include \u003clinux/kapi_syscall.h\u003e\n+#endif\n+\n #ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER\n /*\n  * It may be useful for an architecture to override the definitions of the\n@@ -237,6 +241,41 @@ static inline int is_syscall_trace_event(struct trace_event_call *tp_event)\n \n #define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)\n \n+#ifdef CONFIG_KAPI_RUNTIME_CHECKS\n+/*\n+ * Convert integers as (s64)(a) does, and pointers through unsigned long so\n+ * they are not sign-extended on 32-bit. (unsigned long)(t)0 is an integer\n+ * constant expression only if t is an integer type.\n+ */\n+#define __SC_CAST_TO_S64(t, a)\t\t\t\t\t\t\\\n+\t((__force s64)__builtin_choose_expr(\t\t\t\t\\\n+\t\t__is_constexpr((__force unsigned long)(t)0),\t\t\\\n+\t\t(a), (__force unsigned long)(a)))\n+\n+#define __KAPI_DO_SYS(name)\t__do_kapi_sys##name\n+#define __KAPI_SYSCALL_DEFINEx(x, name, ...)\t\t\t\t\\\n+\tstatic inline long __do_kapi_sys##name(__MAP(x, __SC_DECL, __VA_ARGS__)) \\\n+\t{\t\t\t\t\t\t\t\t\\\n+\t\tconst struct kernel_api_spec *__spec = kapi_get_spec(\"sys\" #name); \\\n+\t\tlong ret;\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t\t\t\\\n+\t\tif (__spec) {\t\t\t\t\t\t\\\n+\t\t\ts64 __params[x] = { __MAP(x, __SC_CAST_TO_S64, __VA_ARGS__) }; \\\n+\t\t\tint __ret = kapi_validate_syscall_params(__spec, __params, x); \\\n+\t\t\t\t\t\t\t\t\t\\\n+\t\t\tif (__ret)\t\t\t\t\t\\\n+\t\t\t\treturn __ret;\t\t\t\t\\\n+\t\t}\t\t\t\t\t\t\t\\\n+\t\tret = __do_sys##name(__MAP(x, __SC_ARGS, __VA_ARGS__));\t\\\n+\t\tif (__spec)\t\t\t\t\t\t\\\n+\t\t\tkapi_validate_syscall_return(__spec, (s64)ret);\t\\\n+\t\treturn ret;\t\t\t\t\t\t\\\n+\t}\n+#else\n+#define __KAPI_DO_SYS(name)\t__do_sys##name\n+#define __KAPI_SYSCALL_DEFINEx(x, name, ...)\n+#endif\n+\n /*\n  * The asmlinkage stub is aliased to a function named __se_sys_*() which\n  * sign-extends 32-bit ints to longs whenever needed. The actual work is\n@@ -255,10 +294,11 @@ static inline int is_syscall_trace_event(struct trace_event_call *tp_event)\n \t\t__attribute__((alias(__stringify(__se_sys##name))));\t\\\n \tALLOW_ERROR_INJECTION(sys##name, ERRNO);\t\t\t\\\n \tstatic inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\\\n+\t__KAPI_SYSCALL_DEFINEx(x, name, __VA_ARGS__)\t\t\t\\\n \tasmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));\t\\\n \tasmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))\t\\\n \t{\t\t\t\t\t\t\t\t\\\n-\t\tlong ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\\\n+\t\tlong ret = __KAPI_DO_SYS(name)(__MAP(x,__SC_CAST,__VA_ARGS__));\\\n \t\t__MAP(x,__SC_TEST,__VA_ARGS__);\t\t\t\t\\\n \t\t__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));\t\\\n \t\treturn ret;\t\t\t\t\t\t\\\ndiff --git a/include/trace/events/kapi.h b/include/trace/events/kapi.h\nnew file mode 100644\nindex 0000000000000..47828f3338828\n--- /dev/null\n+++ b/include/trace/events/kapi.h\n@@ -0,0 +1,74 @@\n+/* SPDX-License-Identifier: GPL-2.0 */\n+#undef TRACE_SYSTEM\n+#define TRACE_SYSTEM kapi\n+\n+#if !defined(_TRACE_KAPI_H) || defined(TRACE_HEADER_MULTI_READ)\n+#define _TRACE_KAPI_H\n+\n+#include \u003clinux/tracepoint.h\u003e\n+\n+/* Max length of the rendered \"name=value, ...\" parameter list. */\n+#define KAPI_TP_PARAMS_LEN 256\n+\n+/*\n+ * Emitted from the CONFIG_KAPI_RUNTIME_CHECKS syscall validation path for\n+ * syscalls that have a KAPI specification: kapi_syscall_enter fires before\n+ * parameter validation, kapi_syscall_exit after the handler returns.\n+ * @name is the spec name, e.g. \"sys_open\".\n+ *\n+ * kapi_syscall_enter carries both the raw argument values (args[]) and, when\n+ * the spec provides parameter metadata, a rendered \"name=value\" list (params,\n+ * built by the caller): pointer-like values in hex, integers and fds in decimal.\n+ */\n+TRACE_EVENT(kapi_syscall_enter,\n+\n+\tTP_PROTO(const char *name, int nargs, const s64 *args, const char *params),\n+\n+\tTP_ARGS(name, nargs, args, params),\n+\n+\tTP_STRUCT__entry(\n+\t\t__string(\tname,\tname\t)\n+\t\t__field(\tint,\tnargs\t)\n+\t\t__array(\tu64,\targs,\t6\t)\n+\t\t__string(\tparams,\tparams\t)\n+\t),\n+\n+\tTP_fast_assign(\n+\t\t__assign_str(name);\n+\t\t__entry-\u003enargs = nargs;\n+\t\tmemset(__entry-\u003eargs, 0, sizeof(__entry-\u003eargs));\n+\t\tif (args \u0026\u0026 nargs \u003e 0)\n+\t\t\tmemcpy(__entry-\u003eargs, args,\n+\t\t\t       min_t(int, nargs, 6) * sizeof(__entry-\u003eargs[0]));\n+\t\t__assign_str(params);\n+\t),\n+\n+\tTP_printk(\"%s(%s)\", __get_str(name), __get_str(params))\n+);\n+\n+TRACE_EVENT(kapi_syscall_exit,\n+\n+\tTP_PROTO(const char *name, long ret, bool spec_match),\n+\n+\tTP_ARGS(name, ret, spec_match),\n+\n+\tTP_STRUCT__entry(\n+\t\t__string(\tname,\t\tname\t\t)\n+\t\t__field(\tlong,\t\tret\t\t)\n+\t\t__field(\tbool,\t\tspec_match\t)\n+\t),\n+\n+\tTP_fast_assign(\n+\t\t__assign_str(name);\n+\t\t__entry-\u003eret = ret;\n+\t\t__entry-\u003espec_match = spec_match;\n+\t),\n+\n+\tTP_printk(\"%s = %ld spec_match=%d\",\n+\t\t  __get_str(name), __entry-\u003eret, __entry-\u003espec_match)\n+);\n+\n+#endif /* _TRACE_KAPI_H */\n+\n+/* This part must be outside protection */\n+#include \u003ctrace/define_trace.h\u003e\ndiff --git a/init/Kconfig b/init/Kconfig\nindex ad592fdf29af4..66c23c4147498 100644\n--- a/init/Kconfig\n+++ b/init/Kconfig\n@@ -2306,6 +2306,8 @@ source \"kernel/Kconfig.kexec\"\n \n source \"kernel/liveupdate/Kconfig\"\n \n+source \"kernel/api/Kconfig\"\n+\n endmenu\t\t# General setup\n \n source \"arch/Kconfig\"\ndiff --git a/kernel/Makefile b/kernel/Makefile\nindex 1e1a31673577d..7fd43127d5eee 100644\n--- a/kernel/Makefile\n+++ b/kernel/Makefile\n@@ -59,6 +59,7 @@ obj-y += dma/\n obj-y += entry/\n obj-y += unwind/\n obj-$(CONFIG_MODULES) += module/\n+obj-$(CONFIG_KAPI_SPEC) += api/\n \n obj-$(CONFIG_KCMP) += kcmp.o\n obj-$(CONFIG_FREEZER) += freezer.o\ndiff --git a/kernel/api/Kconfig b/kernel/api/Kconfig\nnew file mode 100644\nindex 0000000000000..6075ab82bd231\n--- /dev/null\n+++ b/kernel/api/Kconfig\n@@ -0,0 +1,74 @@\n+# SPDX-License-Identifier: GPL-2.0-only\n+#\n+# Kernel API Specification Framework Configuration\n+#\n+\n+config KAPI_SPEC\n+\tbool \"Kernel API Specification Framework\"\n+\thelp\n+\t  This option enables the kernel API specification framework,\n+\t  which provides formal documentation of kernel APIs in both\n+\t  human and machine-readable formats.\n+\n+\t  The framework allows developers to document APIs inline with\n+\t  their implementation, including parameter specifications,\n+\t  return values, error conditions, locking requirements, and\n+\t  execution context constraints.\n+\n+\t  When enabled, API specifications can be queried at runtime\n+\t  and exported in JSON format through debugfs.\n+\n+\t  If unsure, say N.\n+\n+config KAPI_RUNTIME_CHECKS\n+\tbool \"Runtime API specification checks\"\n+\tdepends on KAPI_SPEC\n+\tdepends on DEBUG_KERNEL\n+\t# The hook is only in the generic and the x86 __SYSCALL_DEFINEx()\n+\tdepends on X86 || !ARCH_HAS_SYSCALL_WRAPPER\n+\thelp\n+\t  Validate the arguments and the return value of system calls that\n+\t  have an API specification, in their SYSCALL_DEFINEx() wrapper.\n+\t  Violations are reported with pr_warn_ratelimited(). This adds\n+\t  overhead to every system call.\n+\n+\t  DEBUG-ONLY: Enabling this changes the errno seen by userspace for\n+\t  syscalls that violate their parameter specification. On violation\n+\t  the validator short-circuits the syscall and returns -EINVAL\n+\t  before the real handler runs, masking whatever errno the handler\n+\t  would otherwise have produced. Do not enable on production\n+\t  kernels.\n+\n+\t  If unsure, say N.\n+\n+config KAPI_SPEC_DEBUGFS\n+\tbool \"Export kernel API specifications via debugfs\"\n+\tdepends on KAPI_SPEC\n+\tdepends on DEBUG_FS\n+\thelp\n+\t  This option enables exporting kernel API specifications through\n+\t  the debugfs filesystem. When enabled, specifications can be\n+\t  accessed at /sys/kernel/debug/kapi/.\n+\n+\t  The debugfs interface provides:\n+\t  - A list of all available API specifications\n+\t  - Detailed information for each API including parameters,\n+\t    return values, errors, locking requirements, and constraints\n+\t  - Complete machine-readable representation of the specs\n+\n+\t  This is useful for documentation tools, static analyzers, and\n+\t  runtime introspection of kernel APIs.\n+\n+\t  If unsure, say N.\n+\n+config KAPI_KUNIT_TEST\n+\ttristate \"KUnit tests for KAPI framework\" if !KUNIT_ALL_TESTS\n+\tdepends on KAPI_SPEC\n+\tdepends on KUNIT\n+\tdefault KUNIT_ALL_TESTS\n+\thelp\n+\t  KUnit tests for the Kernel API Specification Framework.\n+\t  Tests registration, lookup, validation constraints, and\n+\t  JSON export functionality.\n+\n+\t  If unsure, say N.\ndiff --git a/kernel/api/Makefile b/kernel/api/Makefile\nnew file mode 100644\nindex 0000000000000..2e1c3555d5aa5\n--- /dev/null\n+++ b/kernel/api/Makefile\n@@ -0,0 +1,13 @@\n+# SPDX-License-Identifier: GPL-2.0\n+#\n+# Makefile for the Kernel API Specification Framework\n+#\n+\n+# Core API specification framework\n+obj-y\t\t\t\t+= kernel_api_spec.o\n+\n+# Debugfs interface for kernel API specs\n+obj-$(CONFIG_KAPI_SPEC_DEBUGFS) += kapi_debugfs.o\n+\n+# KUnit tests\n+obj-$(CONFIG_KAPI_KUNIT_TEST) += kapi_kunit.o\ndiff --git a/kernel/api/internal.h b/kernel/api/internal.h\nnew file mode 100644\nindex 0000000000000..b6112bcc2e8d0\n--- /dev/null\n+++ b/kernel/api/internal.h\n@@ -0,0 +1,25 @@\n+/* SPDX-License-Identifier: GPL-2.0 */\n+/*\n+ * Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+ *\n+ * Internal declarations shared by the KAPI core and its debugfs\n+ * interface. Not part of the public kernel API.\n+ */\n+\n+#ifndef _KERNEL_API_INTERNAL_H\n+#define _KERNEL_API_INTERNAL_H\n+\n+#include \u003clinux/kernel_api_spec.h\u003e\n+\n+/*\n+ * Section boundaries for the `.kapi_specs` array. Defined by the\n+ * linker script in include/asm-generic/vmlinux.lds.h.\n+ */\n+extern const struct kernel_api_spec * const __start_kapi_specs[];\n+extern const struct kernel_api_spec * const __stop_kapi_specs[];\n+\n+const char *kapi_param_type_to_string(enum kapi_param_type type);\n+const char *kapi_lock_type_to_string(enum kapi_lock_type type);\n+const char *kapi_lock_scope_to_string(enum kapi_lock_scope scope);\n+\n+#endif /* _KERNEL_API_INTERNAL_H */\ndiff --git a/kernel/api/kapi_debugfs.c b/kernel/api/kapi_debugfs.c\nnew file mode 100644\nindex 0000000000000..5d9c2649a0dc6\n--- /dev/null\n+++ b/kernel/api/kapi_debugfs.c\n@@ -0,0 +1,553 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+ *\n+ * Kernel API specification debugfs interface\n+ *\n+ * This provides a debugfs interface to expose kernel API specifications\n+ * at runtime, allowing tools and users to query the complete API specs.\n+ */\n+\n+#include \u003clinux/debugfs.h\u003e\n+#include \u003clinux/kernel.h\u003e\n+#include \u003clinux/init.h\u003e\n+#include \u003clinux/seq_file.h\u003e\n+#include \u003clinux/kernel_api_spec.h\u003e\n+#include \u003clinux/slab.h\u003e\n+#include \u003clinux/string.h\u003e\n+\n+#include \"internal.h\"\n+\n+/* Helper to print parameter flags */\n+static void print_param_flags(struct seq_file *m, u32 flags)\n+{\n+\tseq_puts(m, \"    flags: \");\n+\tif ((flags \u0026 KAPI_PARAM_INOUT) == KAPI_PARAM_INOUT)\n+\t\tseq_puts(m, \"INOUT \");\n+\telse if (flags \u0026 KAPI_PARAM_IN)\n+\t\tseq_puts(m, \"IN \");\n+\telse if (flags \u0026 KAPI_PARAM_OUT)\n+\t\tseq_puts(m, \"OUT \");\n+\tif (flags \u0026 KAPI_PARAM_OPTIONAL)\n+\t\tseq_puts(m, \"OPTIONAL \");\n+\tif (flags \u0026 KAPI_PARAM_CONST)\n+\t\tseq_puts(m, \"CONST \");\n+\tif (flags \u0026 KAPI_PARAM_USER)\n+\t\tseq_puts(m, \"USER \");\n+\tif (flags \u0026 KAPI_PARAM_VOLATILE)\n+\t\tseq_puts(m, \"VOLATILE \");\n+\tif (flags \u0026 KAPI_PARAM_DMA)\n+\t\tseq_puts(m, \"DMA \");\n+\tif (flags \u0026 KAPI_PARAM_ALIGNED)\n+\t\tseq_puts(m, \"ALIGNED \");\n+\tseq_puts(m, \"\\n\");\n+}\n+\n+/* Helper to print context flags */\n+static void print_context_flags(struct seq_file *m, u32 flags)\n+{\n+\tseq_puts(m, \"Context flags: \");\n+\tif (flags \u0026 KAPI_CTX_PROCESS)\n+\t\tseq_puts(m, \"PROCESS \");\n+\tif (flags \u0026 KAPI_CTX_HARDIRQ)\n+\t\tseq_puts(m, \"HARDIRQ \");\n+\tif (flags \u0026 KAPI_CTX_SOFTIRQ)\n+\t\tseq_puts(m, \"SOFTIRQ \");\n+\tif (flags \u0026 KAPI_CTX_NMI)\n+\t\tseq_puts(m, \"NMI \");\n+\tif (flags \u0026 KAPI_CTX_SLEEPABLE)\n+\t\tseq_puts(m, \"SLEEPABLE \");\n+\tif (flags \u0026 KAPI_CTX_ATOMIC)\n+\t\tseq_puts(m, \"ATOMIC \");\n+\tif (flags \u0026 KAPI_CTX_PREEMPT_DISABLED)\n+\t\tseq_puts(m, \"PREEMPT_DISABLED \");\n+\tif (flags \u0026 KAPI_CTX_IRQ_DISABLED)\n+\t\tseq_puts(m, \"IRQ_DISABLED \");\n+\tseq_puts(m, \"\\n\");\n+}\n+\n+/*\n+ * Print a multi-line value with every line indented, so that no line of it\n+ * can be mistaken for a field of its own.\n+ */\n+static void print_block(struct seq_file *m, const char *text)\n+{\n+\twhile (*text) {\n+\t\tsize_t len = strcspn(text, \"\\n\");\n+\n+\t\tif (len)\n+\t\t\tseq_printf(m, \"  %.*s\", (int)len, text);\n+\t\tseq_putc(m, '\\n');\n+\t\ttext += len;\n+\t\tif (*text == '\\n')\n+\t\t\ttext++;\n+\t}\n+}\n+\n+/* Helper to print a list of signed values */\n+static void print_s64_list(struct seq_file *m, const char *label,\n+\t\t\t   const s64 *vals, u32 count)\n+{\n+\tu32 i;\n+\n+\tif (!vals || !count)\n+\t\treturn;\n+\n+\tseq_printf(m, \"%s:\", label);\n+\tfor (i = 0; i \u003c count; i++)\n+\t\tseq_printf(m, \" %lld\", vals[i]);\n+\tseq_puts(m, \"\\n\");\n+}\n+\n+/* Helper to print which parameter carries a buffer's element count */\n+static void print_size_param(struct seq_file *m,\n+\t\t\t     const struct kernel_api_spec *spec,\n+\t\t\t     const struct kapi_param_spec *param)\n+{\n+\tint idx = param-\u003esize_param_idx - 1;\n+\n+\tif (idx \u003c 0)\n+\t\treturn;\n+\n+\tseq_printf(m, \"      size_param: %s (index %d)\\n\",\n+\t\t   idx \u003c spec-\u003eparam_count \u0026\u0026 idx \u003c KAPI_MAX_PARAMS ?\n+\t\t   spec-\u003eparams[idx].name : \"?\", idx);\n+\tif (param-\u003esize_multiplier)\n+\t\tseq_printf(m, \"      size_multiplier: %zu\\n\",\n+\t\t\t   param-\u003esize_multiplier);\n+}\n+\n+/* Show function for individual API spec */\n+static int kapi_spec_show(struct seq_file *m, void *v)\n+{\n+\tstruct kernel_api_spec *spec = m-\u003eprivate;\n+\tint i;\n+\n+\tseq_puts(m, \"Kernel API Specification\\n\");\n+\tseq_puts(m, \"========================\\n\\n\");\n+\n+\t/* Basic info */\n+\tseq_printf(m, \"Name: %s\\n\", spec-\u003ename);\n+\tseq_printf(m, \"Version: %u\\n\", spec-\u003eversion);\n+\tseq_printf(m, \"Description: %s\\n\", spec-\u003edescription);\n+\tif (spec-\u003elong_description \u0026\u0026 *spec-\u003elong_description) {\n+\t\tseq_puts(m, \"Long description:\\n\");\n+\t\tprint_block(m, spec-\u003elong_description);\n+\t}\n+\n+\t/* Context */\n+\tprint_context_flags(m, spec-\u003econtext_flags);\n+\tseq_puts(m, \"\\n\");\n+\n+\t/* Parameters */\n+\tif (spec-\u003eparam_count \u003e 0) {\n+\t\tseq_printf(m, \"Parameters (%u):\\n\", spec-\u003eparam_count);\n+\t\tfor (i = 0; i \u003c spec-\u003eparam_count \u0026\u0026 i \u003c KAPI_MAX_PARAMS; i++) {\n+\t\t\tstruct kapi_param_spec *param = \u0026spec-\u003eparams[i];\n+\n+\t\t\tseq_printf(m, \"  [%d] %s:\\n\", i, param-\u003ename);\n+\t\t\tseq_printf(m, \"    type: %s (%s)\\n\",\n+\t\t\t\t   kapi_param_type_to_string(param-\u003etype), param-\u003etype_name);\n+\t\t\tprint_param_flags(m, param-\u003eflags);\n+\t\t\tif (param-\u003edescription \u0026\u0026 *param-\u003edescription)\n+\t\t\t\tseq_printf(m, \"    description: %s\\n\", param-\u003edescription);\n+\t\t\tif (param-\u003esize \u003e 0)\n+\t\t\t\tseq_printf(m, \"    size: %zu\\n\", param-\u003esize);\n+\t\t\tif (param-\u003ealignment \u003e 0)\n+\t\t\t\tseq_printf(m, \"    alignment: %zu\\n\", param-\u003ealignment);\n+\n+\t\t\t/* Print constraints if any */\n+\t\t\tif (param-\u003econstraint_type != KAPI_CONSTRAINT_NONE ||\n+\t\t\t    (param-\u003econstraints \u0026\u0026 *param-\u003econstraints)) {\n+\t\t\t\tseq_puts(m, \"    constraints:\\n\");\n+\t\t\t\tswitch (param-\u003econstraint_type) {\n+\t\t\t\tcase KAPI_CONSTRAINT_NONE:\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_RANGE:\n+\t\t\t\t\tseq_puts(m, \"      type: range\\n\");\n+\t\t\t\t\tseq_printf(m, \"      min: %lld\\n\", param-\u003emin_value);\n+\t\t\t\t\tseq_printf(m, \"      max: %lld\\n\", param-\u003emax_value);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_MASK:\n+\t\t\t\t\tseq_puts(m, \"      type: mask\\n\");\n+\t\t\t\t\tseq_printf(m, \"      valid_bits: 0x%llx\\n\",\n+\t\t\t\t\t\t   param-\u003evalid_mask);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_ENUM:\n+\t\t\t\t\tseq_puts(m, \"      type: enum\\n\");\n+\t\t\t\t\tseq_printf(m, \"      count: %u\\n\", param-\u003eenum_count);\n+\t\t\t\t\tprint_s64_list(m, \"      values\",\n+\t\t\t\t\t\t       param-\u003eenum_values,\n+\t\t\t\t\t\t       param-\u003eenum_count);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_USER_STRING:\n+\t\t\t\t\tseq_puts(m, \"      type: user_string\\n\");\n+\t\t\t\t\tseq_printf(m, \"      min_len: %lld\\n\", param-\u003emin_value);\n+\t\t\t\t\tseq_printf(m, \"      max_len: %lld\\n\", param-\u003emax_value);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_USER_PATH:\n+\t\t\t\t\tseq_puts(m, \"      type: user_path\\n\");\n+\t\t\t\t\tseq_puts(m, \"      max_len: PATH_MAX (4096)\\n\");\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_USER_PTR:\n+\t\t\t\t\tseq_puts(m, \"      type: user_ptr\\n\");\n+\t\t\t\t\tseq_printf(m, \"      size: %zu bytes\\n\", param-\u003esize);\n+\t\t\t\t\tprint_size_param(m, spec, param);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_BUFFER:\n+\t\t\t\t\tseq_puts(m, \"      type: buffer\\n\");\n+\t\t\t\t\tprint_size_param(m, spec, param);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_ALIGNMENT:\n+\t\t\t\t\tseq_puts(m, \"      type: alignment\\n\");\n+\t\t\t\t\tseq_printf(m, \"      alignment: %zu\\n\", param-\u003ealignment);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_POWER_OF_TWO:\n+\t\t\t\t\tseq_puts(m, \"      type: power_of_two\\n\");\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_PAGE_ALIGNED:\n+\t\t\t\t\tseq_puts(m, \"      type: page_aligned\\n\");\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_NONZERO:\n+\t\t\t\t\tseq_puts(m, \"      type: nonzero\\n\");\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase KAPI_CONSTRAINT_CUSTOM:\n+\t\t\t\t\tseq_puts(m, \"      type: custom\\n\");\n+\t\t\t\t\tbreak;\n+\t\t\t\tdefault:\n+\t\t\t\t\tseq_printf(m, \"      type: unknown (%d)\\n\",\n+\t\t\t\t\t\t   param-\u003econstraint_type);\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t\tif (param-\u003econstraints \u0026\u0026 *param-\u003econstraints)\n+\t\t\t\t\tseq_printf(m, \"      description: %s\\n\",\n+\t\t\t\t\t\t   param-\u003econstraints);\n+\t\t\t}\n+\t\t\tseq_puts(m, \"\\n\");\n+\t\t}\n+\t}\n+\n+\t/* Return value */\n+\tseq_puts(m, \"Return value:\\n\");\n+\tseq_printf(m, \"  type: %s\\n\", spec-\u003ereturn_spec.type_name);\n+\tif (spec-\u003ereturn_spec.description \u0026\u0026 *spec-\u003ereturn_spec.description)\n+\t\tseq_printf(m, \"  description: %s\\n\", spec-\u003ereturn_spec.description);\n+\n+\tswitch (spec-\u003ereturn_spec.check_type) {\n+\tcase KAPI_RETURN_EXACT:\n+\t\tseq_printf(m, \"  success: == %lld\\n\", spec-\u003ereturn_spec.success_value);\n+\t\tbreak;\n+\tcase KAPI_RETURN_RANGE:\n+\t\tseq_printf(m, \"  success: [%lld, %lld]\\n\",\n+\t\t\t   spec-\u003ereturn_spec.success_min,\n+\t\t\t   spec-\u003ereturn_spec.success_max);\n+\t\tbreak;\n+\tcase KAPI_RETURN_FD:\n+\t\tseq_puts(m, \"  success: valid file descriptor (\u003e= 0)\\n\");\n+\t\tbreak;\n+\tcase KAPI_RETURN_ERROR_CHECK:\n+\t\tseq_puts(m, \"  success: error check\\n\");\n+\t\tprint_s64_list(m, \"  error values\",\n+\t\t\t       spec-\u003ereturn_spec.error_values,\n+\t\t\t       spec-\u003ereturn_spec.error_count);\n+\t\tbreak;\n+\tcase KAPI_RETURN_CUSTOM:\n+\t\tseq_puts(m, \"  success: custom check\\n\");\n+\t\tbreak;\n+\tcase KAPI_RETURN_NO_RETURN:\n+\t\tseq_puts(m, \"  success: does not return\\n\");\n+\t\tbreak;\n+\tdefault:\n+\t\tbreak;\n+\t}\n+\tseq_puts(m, \"\\n\");\n+\n+\t/* Errors */\n+\tif (spec-\u003eerror_count \u003e 0) {\n+\t\tseq_printf(m, \"Errors (%u):\\n\", spec-\u003eerror_count);\n+\t\tfor (i = 0; i \u003c spec-\u003eerror_count \u0026\u0026 i \u003c KAPI_MAX_ERRORS; i++) {\n+\t\t\tstruct kapi_error_spec *err = \u0026spec-\u003eerrors[i];\n+\n+\t\t\tseq_printf(m, \"  %s (%d): %s\\n\",\n+\t\t\t\t   err-\u003ename, err-\u003eerror_code, err-\u003edescription);\n+\t\t\tif (err-\u003econdition \u0026\u0026 *err-\u003econdition)\n+\t\t\t\tseq_printf(m, \"    condition: %s\\n\", err-\u003econdition);\n+\t\t}\n+\t\tseq_puts(m, \"\\n\");\n+\t}\n+\n+\t/* Locks */\n+\tif (spec-\u003elock_count \u003e 0) {\n+\t\tseq_printf(m, \"Locks (%u):\\n\", spec-\u003elock_count);\n+\t\tfor (i = 0; i \u003c spec-\u003elock_count \u0026\u0026 i \u003c KAPI_MAX_LOCKS; i++) {\n+\t\t\tstruct kapi_lock_spec *lock = \u0026spec-\u003elocks[i];\n+\n+\t\t\tseq_printf(m, \"  %s (%s): %s\\n\", lock-\u003elock_name,\n+\t\t\t\t   kapi_lock_type_to_string(lock-\u003elock_type),\n+\t\t\t\t   lock-\u003edescription);\n+\t\t\tseq_printf(m, \"    scope: %s\\n\",\n+\t\t\t\t   kapi_lock_scope_to_string(lock-\u003escope));\n+\t\t}\n+\t\tseq_puts(m, \"\\n\");\n+\t}\n+\n+\t/* Constraints */\n+\tif (spec-\u003econstraint_count \u003e 0) {\n+\t\tseq_printf(m, \"Additional constraints (%u):\\n\", spec-\u003econstraint_count);\n+\t\tfor (i = 0; i \u003c spec-\u003econstraint_count \u0026\u0026 i \u003c KAPI_MAX_CONSTRAINTS; i++) {\n+\t\t\tstruct kapi_constraint_spec *cons = \u0026spec-\u003econstraints[i];\n+\n+\t\t\tseq_printf(m, \"  - %s\", cons-\u003ename);\n+\t\t\tif (cons-\u003edescription \u0026\u0026 *cons-\u003edescription)\n+\t\t\t\tseq_printf(m, \": %s\", cons-\u003edescription);\n+\t\t\tseq_puts(m, \"\\n\");\n+\t\t\tif (cons-\u003eexpression \u0026\u0026 *cons-\u003eexpression)\n+\t\t\t\tseq_printf(m, \"    expression: %s\\n\", cons-\u003eexpression);\n+\t\t}\n+\t\tseq_puts(m, \"\\n\");\n+\t}\n+\n+\t/* Signals */\n+\tif (spec-\u003esignal_count \u003e 0) {\n+\t\tseq_printf(m, \"Signal handling (%u):\\n\", spec-\u003esignal_count);\n+\t\tfor (i = 0; i \u003c spec-\u003esignal_count \u0026\u0026 i \u003c KAPI_MAX_SIGNALS; i++) {\n+\t\t\tstruct kapi_signal_spec *sig = \u0026spec-\u003esignals[i];\n+\n+\t\t\tseq_printf(m, \"  %s (%d):\\n\", sig-\u003esignal_name, sig-\u003esignal_num);\n+\t\t\tseq_puts(m, \"    direction: \");\n+\t\t\tif (sig-\u003edirection \u0026 KAPI_SIGNAL_SEND)\n+\t\t\t\tseq_puts(m, \"send \");\n+\t\t\tif (sig-\u003edirection \u0026 KAPI_SIGNAL_RECEIVE)\n+\t\t\t\tseq_puts(m, \"receive \");\n+\t\t\tif (sig-\u003edirection \u0026 KAPI_SIGNAL_HANDLE)\n+\t\t\t\tseq_puts(m, \"handle \");\n+\t\t\tif (sig-\u003edirection \u0026 KAPI_SIGNAL_BLOCK)\n+\t\t\t\tseq_puts(m, \"block \");\n+\t\t\tif (sig-\u003edirection \u0026 KAPI_SIGNAL_IGNORE)\n+\t\t\t\tseq_puts(m, \"ignore \");\n+\t\t\tseq_puts(m, \"\\n\");\n+\t\t\tseq_puts(m, \"    action: \");\n+\t\t\tswitch (sig-\u003eaction) {\n+\t\t\tcase KAPI_SIGNAL_ACTION_DEFAULT:\n+\t\t\t\tseq_puts(m, \"default\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_TERMINATE:\n+\t\t\t\tseq_puts(m, \"terminate\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_COREDUMP:\n+\t\t\t\tseq_puts(m, \"coredump\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_STOP:\n+\t\t\t\tseq_puts(m, \"stop\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_CONTINUE:\n+\t\t\t\tseq_puts(m, \"continue\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_CUSTOM:\n+\t\t\t\tseq_puts(m, \"custom\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_RETURN:\n+\t\t\t\tseq_puts(m, \"return\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_RESTART:\n+\t\t\t\tseq_puts(m, \"restart\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_QUEUE:\n+\t\t\t\tseq_puts(m, \"queue\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_DISCARD:\n+\t\t\t\tseq_puts(m, \"discard\");\n+\t\t\t\tbreak;\n+\t\t\tcase KAPI_SIGNAL_ACTION_TRANSFORM:\n+\t\t\t\tseq_puts(m, \"transform\");\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tseq_puts(m, \"unknown\");\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tseq_puts(m, \"\\n\");\n+\t\t\tif (sig-\u003edescription \u0026\u0026 *sig-\u003edescription)\n+\t\t\t\tseq_printf(m, \"    description: %s\\n\", sig-\u003edescription);\n+\t\t}\n+\t\tseq_puts(m, \"\\n\");\n+\t}\n+\n+\t/* Side effects */\n+\tif (spec-\u003eside_effect_count \u003e 0) {\n+\t\tseq_printf(m, \"Side effects (%u):\\n\", spec-\u003eside_effect_count);\n+\t\tfor (i = 0; i \u003c spec-\u003eside_effect_count \u0026\u0026 i \u003c KAPI_MAX_SIDE_EFFECTS; i++) {\n+\t\t\tconst struct kapi_side_effect *eff = \u0026spec-\u003eside_effects[i];\n+\n+\t\t\tseq_printf(m, \"  - %s\", eff-\u003etarget);\n+\t\t\tif (eff-\u003edescription \u0026\u0026 *eff-\u003edescription)\n+\t\t\t\tseq_printf(m, \": %s\", eff-\u003edescription);\n+\t\t\tif (eff-\u003ereversible)\n+\t\t\t\tseq_puts(m, \" (reversible)\");\n+\t\t\tseq_puts(m, \"\\n\");\n+\t\t}\n+\t\tseq_puts(m, \"\\n\");\n+\t}\n+\n+\t/* State transitions */\n+\tif (spec-\u003estate_trans_count \u003e 0) {\n+\t\tseq_printf(m, \"State transitions (%u):\\n\", spec-\u003estate_trans_count);\n+\t\tfor (i = 0; i \u003c spec-\u003estate_trans_count \u0026\u0026 i \u003c KAPI_MAX_STATE_TRANS; i++) {\n+\t\t\tconst struct kapi_state_transition *trans = \u0026spec-\u003estate_transitions[i];\n+\n+\t\t\tseq_printf(m, \"  %s: %s -\u003e %s\\n\", trans-\u003eobject,\n+\t\t\t\t   trans-\u003efrom_state, trans-\u003eto_state);\n+\t\t\tif (trans-\u003edescription \u0026\u0026 *trans-\u003edescription)\n+\t\t\t\tseq_printf(m, \"    %s\\n\", trans-\u003edescription);\n+\t\t}\n+\t\tseq_puts(m, \"\\n\");\n+\t}\n+\n+\t/* Capabilities */\n+\tif (spec-\u003ecapability_count \u003e 0) {\n+\t\tseq_printf(m, \"Capabilities (%u):\\n\", spec-\u003ecapability_count);\n+\t\tfor (i = 0; i \u003c spec-\u003ecapability_count \u0026\u0026 i \u003c KAPI_MAX_CAPABILITIES; i++) {\n+\t\t\tconst struct kapi_capability_spec *cap = \u0026spec-\u003ecapabilities[i];\n+\n+\t\t\tseq_printf(m, \"  %s (%d):\\n\", cap-\u003ecap_name,\n+\t\t\t\t   cap-\u003ecapability);\n+\t\t\tif (cap-\u003eallows \u0026\u0026 *cap-\u003eallows)\n+\t\t\t\tseq_printf(m, \"    allows: %s\\n\", cap-\u003eallows);\n+\t\t\tif (cap-\u003ewithout_cap \u0026\u0026 *cap-\u003ewithout_cap)\n+\t\t\t\tseq_printf(m, \"    without: %s\\n\", cap-\u003ewithout_cap);\n+\t\t}\n+\t\tseq_puts(m, \"\\n\");\n+\t}\n+\n+\t/* Additional info */\n+\tif (spec-\u003eexamples \u0026\u0026 *spec-\u003eexamples) {\n+\t\tseq_puts(m, \"Examples:\\n\");\n+\t\tprint_block(m, spec-\u003eexamples);\n+\t\tseq_putc(m, '\\n');\n+\t}\n+\tif (spec-\u003enotes \u0026\u0026 *spec-\u003enotes) {\n+\t\tseq_puts(m, \"Notes:\\n\");\n+\t\tprint_block(m, spec-\u003enotes);\n+\t\tseq_putc(m, '\\n');\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int kapi_spec_open(struct inode *inode, struct file *file)\n+{\n+\treturn single_open(file, kapi_spec_show, inode-\u003ei_private);\n+}\n+\n+static const struct file_operations kapi_spec_fops = {\n+\t.open = kapi_spec_open,\n+\t.read = seq_read,\n+\t.llseek = seq_lseek,\n+\t.release = single_release,\n+};\n+\n+/*\n+ * JSON view of a single spec.  kapi_export_json() writes straight into the\n+ * seq_file buffer; when the output does not fit, flagging an overflow makes\n+ * seq_file retry with a larger buffer, so no fixed size limit applies.\n+ */\n+static int kapi_spec_json_show(struct seq_file *m, void *v)\n+{\n+\tconst struct kernel_api_spec *spec = m-\u003eprivate;\n+\tsize_t size;\n+\tchar *buf;\n+\tint ret;\n+\n+\tif (!spec)\n+\t\treturn -EINVAL;\n+\n+\tsize = seq_get_buf(m, \u0026buf);\n+\tret = size ? kapi_export_json(spec, buf, size) : -E2BIG;\n+\tif (ret == -E2BIG) {\n+\t\tseq_commit(m, -1);\n+\t\treturn 0;\n+\t}\n+\tif (ret \u003c 0)\n+\t\treturn ret;\n+\n+\tseq_commit(m, ret);\n+\treturn 0;\n+}\n+\n+static int kapi_spec_json_open(struct inode *inode, struct file *file)\n+{\n+\treturn single_open(file, kapi_spec_json_show, inode-\u003ei_private);\n+}\n+\n+static const struct file_operations kapi_spec_json_fops = {\n+\t.open = kapi_spec_json_open,\n+\t.read = seq_read,\n+\t.llseek = seq_lseek,\n+\t.release = single_release,\n+};\n+\n+/*\n+ * Show all available API specs.\n+ *\n+ * Note: This only iterates the static .kapi_specs section. Specs registered\n+ * dynamically via kapi_register_spec() are not included in this listing\n+ * or in the per-spec debugfs files.\n+ */\n+static int kapi_list_show(struct seq_file *m, void *v)\n+{\n+\tconst struct kernel_api_spec * const *pp;\n+\tint count = 0;\n+\n+\tseq_puts(m, \"Available Kernel API Specifications\\n\");\n+\tseq_puts(m, \"===================================\\n\\n\");\n+\n+\tfor (pp = __start_kapi_specs; pp \u003c __stop_kapi_specs; pp++) {\n+\t\tconst struct kernel_api_spec *spec = *pp;\n+\n+\t\tif (!spec)\n+\t\t\tcontinue;\n+\t\tseq_printf(m, \"%s - %s\\n\", spec-\u003ename, spec-\u003edescription);\n+\t\tcount++;\n+\t}\n+\n+\tseq_printf(m, \"\\nTotal: %d specifications\\n\", count);\n+\treturn 0;\n+}\n+\n+static int kapi_list_open(struct inode *inode, struct file *file)\n+{\n+\treturn single_open(file, kapi_list_show, NULL);\n+}\n+\n+static const struct file_operations kapi_list_fops = {\n+\t.open = kapi_list_open,\n+\t.read = seq_read,\n+\t.llseek = seq_lseek,\n+\t.release = single_release,\n+};\n+\n+static int __init kapi_debugfs_init(void)\n+{\n+\tconst struct kernel_api_spec * const *pp;\n+\tstruct dentry *root, *spec_dir, *json_dir;\n+\n+\troot = debugfs_create_dir(\"kapi\", NULL);\n+\tdebugfs_create_file(\"list\", 0444, root, NULL, \u0026kapi_list_fops);\n+\tspec_dir = debugfs_create_dir(\"specs\", root);\n+\tjson_dir = debugfs_create_dir(\"specs-json\", root);\n+\n+\tfor (pp = __start_kapi_specs; pp \u003c __stop_kapi_specs; pp++) {\n+\t\tconst struct kernel_api_spec *spec = *pp;\n+\n+\t\tif (!spec || !spec-\u003ename)\n+\t\t\tcontinue;\n+\t\tdebugfs_create_file(spec-\u003ename, 0444, spec_dir,\n+\t\t\t\t    (void *)spec, \u0026kapi_spec_fops);\n+\t\tdebugfs_create_file(spec-\u003ename, 0444, json_dir,\n+\t\t\t\t    (void *)spec, \u0026kapi_spec_json_fops);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+/* Initialize as part of kernel, not as a module */\n+fs_initcall(kapi_debugfs_init);\ndiff --git a/kernel/api/kapi_kunit.c b/kernel/api/kapi_kunit.c\nnew file mode 100644\nindex 0000000000000..4f6f874c33b86\n--- /dev/null\n+++ b/kernel/api/kapi_kunit.c\n@@ -0,0 +1,720 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+ *\n+ * KUnit tests for the Kernel API Specification Framework\n+ *\n+ * Tests registration, lookup, validation, and JSON export functionality.\n+ */\n+\n+#include \u003ckunit/test.h\u003e\n+#include \u003clinux/kernel_api_spec.h\u003e\n+#include \u003clinux/string.h\u003e\n+#include \u003clinux/slab.h\u003e\n+#include \u003clinux/capability.h\u003e\n+#include \u003clinux/mm.h\u003e\n+#include \u003clinux/signal.h\u003e\n+#include \u003clinux/uaccess.h\u003e\n+\n+static void init_test_spec(struct kernel_api_spec *spec, const char *name)\n+{\n+\tmemset(spec, 0, sizeof(*spec));\n+\tspec-\u003ename = name;\n+\tspec-\u003eversion = 1;\n+\tspec-\u003edescription = \"Test API\";\n+}\n+\n+/* kapi_register_spec with valid spec returns 0 */\n+static void test_register_valid(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\tint ret;\n+\n+\tspec = kzalloc_obj(*spec, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tinit_test_spec(spec, \"test_register_valid\");\n+\n+\tret = kapi_register_spec(spec);\n+\tKUNIT_EXPECT_EQ(test, ret, 0);\n+\n+\tkapi_unregister_spec(\"test_register_valid\");\n+\tkfree(spec);\n+}\n+\n+/* kapi_get_spec returns registered spec */\n+static void test_lookup_registered(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\tconst struct kernel_api_spec *found;\n+\tint ret;\n+\n+\tspec = kzalloc_obj(*spec, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tinit_test_spec(spec, \"test_lookup_func\");\n+\n+\tret = kapi_register_spec(spec);\n+\tKUNIT_ASSERT_EQ(test, ret, 0);\n+\n+\tfound = kapi_get_spec(\"test_lookup_func\");\n+\tKUNIT_EXPECT_PTR_EQ(test, found, (const struct kernel_api_spec *)spec);\n+\n+\tkapi_unregister_spec(\"test_lookup_func\");\n+\tkfree(spec);\n+}\n+\n+/* Double registration returns -EEXIST */\n+static void test_double_register(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\tint ret;\n+\n+\tspec = kzalloc_obj(*spec, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tinit_test_spec(spec, \"test_double_reg\");\n+\n+\tret = kapi_register_spec(spec);\n+\tKUNIT_ASSERT_EQ(test, ret, 0);\n+\n+\tret = kapi_register_spec(spec);\n+\tKUNIT_EXPECT_EQ(test, ret, -EEXIST);\n+\n+\tkapi_unregister_spec(\"test_double_reg\");\n+\tkfree(spec);\n+}\n+\n+/* Unregister makes spec unfindable */\n+static void test_unregister(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\tconst struct kernel_api_spec *found;\n+\tint ret;\n+\n+\tspec = kzalloc_obj(*spec, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tinit_test_spec(spec, \"test_unreg_func\");\n+\n+\tret = kapi_register_spec(spec);\n+\tKUNIT_ASSERT_EQ(test, ret, 0);\n+\n+\tkapi_unregister_spec(\"test_unreg_func\");\n+\n+\tfound = kapi_get_spec(\"test_unreg_func\");\n+\tKUNIT_EXPECT_NULL(test, found);\n+\n+\tkfree(spec);\n+}\n+\n+/* kapi_get_spec(NULL) returns NULL */\n+static void test_get_spec_null(struct kunit *test)\n+{\n+\tconst struct kernel_api_spec *found;\n+\n+\tfound = kapi_get_spec(NULL);\n+\tKUNIT_EXPECT_NULL(test, found);\n+}\n+\n+/* kapi_register_spec(NULL) returns -EINVAL */\n+static void test_register_null(struct kunit *test)\n+{\n+\tint ret;\n+\n+\tret = kapi_register_spec(NULL);\n+\tKUNIT_EXPECT_EQ(test, ret, -EINVAL);\n+}\n+\n+/* Spec with a NULL name is rejected */\n+static void test_register_null_name(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\tint ret;\n+\n+\tspec = kzalloc_obj(*spec, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\t/* spec-\u003ename == NULL after zero-init; registration rejects it. */\n+\tret = kapi_register_spec(spec);\n+\tKUNIT_EXPECT_EQ(test, ret, -EINVAL);\n+\n+\tkfree(spec);\n+}\n+\n+#ifdef CONFIG_KAPI_RUNTIME_CHECKS\n+\n+/* RANGE constraint - value in range is valid */\n+static void test_constraint_range_valid(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_param\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_RANGE;\n+\tparam.min_value = 0;\n+\tparam.max_value = 100;\n+\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 50));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 100));\n+\n+\tparam.min_value = -10;\n+\tparam.max_value = -1;\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, -10));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, -1));\n+\n+\t/* An unsigned maximum such as U64_MAX reads as -1 */\n+\tparam.min_value = 0;\n+\tparam.max_value = (s64)U64_MAX;\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, S64_MAX));\n+}\n+\n+/* RANGE constraint - value out of range is invalid */\n+static void test_constraint_range_invalid(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_param\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_RANGE;\n+\tparam.min_value = 0;\n+\tparam.max_value = 100;\n+\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, -1));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 101));\n+\n+\tparam.min_value = -10;\n+\tparam.max_value = -1;\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, -11));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 5));\n+}\n+\n+/* MASK constraint - valid bits pass */\n+static void test_constraint_mask_valid(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_flags\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_MASK;\n+\tparam.valid_mask = 0xFF;\n+\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0x00));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0x0F));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0xFF));\n+}\n+\n+/* MASK constraint - extra bits fail */\n+static void test_constraint_mask_invalid(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_flags\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_MASK;\n+\tparam.valid_mask = 0xFF;\n+\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0x100));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0x1FF));\n+}\n+\n+/* POWER_OF_TWO constraint */\n+static void test_constraint_power_of_two(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_pot\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_POWER_OF_TWO;\n+\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 1));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 2));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 4));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 8));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 3));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 5));\n+}\n+\n+/* PAGE_ALIGNED constraint */\n+static void test_constraint_page_aligned(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_page\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_PAGE_ALIGNED;\n+\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, PAGE_SIZE));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 2 * PAGE_SIZE));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 1));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, PAGE_SIZE - 1));\n+}\n+\n+/* NONZERO constraint */\n+static void test_constraint_nonzero(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_nz\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_NONZERO;\n+\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 1));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, -1));\n+}\n+\n+/* Return value validation - success */\n+static void test_return_validation(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\n+\tspec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tspec-\u003ename = \"test_ret\";\n+\tspec-\u003ereturn_magic = KAPI_MAGIC_RETURN;\n+\tspec-\u003ereturn_spec.check_type = KAPI_RETURN_EXACT;\n+\tspec-\u003ereturn_spec.success_value = 0;\n+\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 0));\n+}\n+\n+/* Return value validation - known error */\n+static void test_return_known_error(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\n+\tspec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tspec-\u003ename = \"test_ret_err\";\n+\tspec-\u003ereturn_magic = KAPI_MAGIC_RETURN;\n+\tspec-\u003ereturn_spec.check_type = KAPI_RETURN_FD;\n+\tspec-\u003eerror_count = 1;\n+\tspec-\u003eerrors[0].error_code = -ENOENT;\n+\tspec-\u003eerrors[0].name = \"ENOENT\";\n+\n+\t/* -ENOENT is in the error list, so it's valid */\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, -ENOENT));\n+}\n+\n+/* Return value validation - unknown error */\n+static void test_return_unknown_error(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\n+\tspec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tspec-\u003ename = \"test_ret_unk\";\n+\tspec-\u003ereturn_magic = KAPI_MAGIC_RETURN;\n+\tspec-\u003ereturn_spec.check_type = KAPI_RETURN_FD;\n+\tspec-\u003eerror_count = 1;\n+\tspec-\u003eerrors[0].error_code = -ENOENT;\n+\tspec-\u003eerrors[0].name = \"ENOENT\";\n+\n+\t/* -EPERM is not in the error list, but unlisted errors are accepted\n+\t * since filesystem/device-specific errors may not be exhaustively listed\n+\t */\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, -EPERM));\n+}\n+\n+/* ALIGNMENT constraint */\n+static void test_constraint_alignment(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_align\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_ALIGNMENT;\n+\tparam.alignment = 8;\n+\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 8));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 16));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 1));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 7));\n+}\n+\n+/* FD validation rejects values \u003e INT_MAX */\n+static void test_fd_int_overflow(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_fd\";\n+\tparam.type = KAPI_TYPE_FD;\n+\tparam.constraint_type = KAPI_CONSTRAINT_NONE;\n+\n+\t/* Value that overflows int: 0x100000003 -\u003e truncates to 3 */\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0x100000003LL));\n+}\n+\n+/* ENUM constraint */\n+static const s64 test_enum_vals[] = { 1, 5, 10 };\n+\n+static void test_constraint_enum(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_enum\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_ENUM;\n+\tparam.enum_values = test_enum_vals;\n+\tparam.enum_count = ARRAY_SIZE(test_enum_vals);\n+\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 1));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 5));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 10));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 3));\n+\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 11));\n+}\n+\n+/* BUFFER constraint always accepts (size checked at runtime) */\n+static void test_constraint_buffer(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tparam.name = \"test_buf\";\n+\tparam.constraint_type = KAPI_CONSTRAINT_BUFFER;\n+\n+\t/* Buffer constraint doesn't validate the value itself */\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 4096));\n+}\n+\n+/* RETURN_RANGE check type */\n+static void test_return_range(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\n+\tspec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tspec-\u003ename = \"test_ret_range\";\n+\tspec-\u003ereturn_magic = KAPI_MAGIC_RETURN;\n+\tspec-\u003ereturn_spec.check_type = KAPI_RETURN_RANGE;\n+\tspec-\u003ereturn_spec.success_min = 0;\n+\tspec-\u003ereturn_spec.success_max = 100;\n+\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 0));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 50));\n+\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 100));\n+}\n+\n+static void init_dyn_buf_param(struct kapi_param_spec *param)\n+{\n+\tparam-\u003ename = \"buf\";\n+\tparam-\u003etype = KAPI_TYPE_USER_PTR;\n+\tparam-\u003econstraint_type = KAPI_CONSTRAINT_BUFFER;\n+\tparam-\u003esize_param_idx = 2;\n+}\n+\n+static bool validate_dyn_buf(const struct kapi_param_spec *param,\n+\t\t\t     const void __user *ptr, s64 count)\n+{\n+\ts64 params[] = { (s64)(unsigned long)ptr, count };\n+\n+\treturn kapi_validate_param_with_context(param, params[0], params,\n+\t\t\t\t\t\tARRAY_SIZE(params));\n+}\n+\n+/* Dynamic buffer: any pointer is accepted when the size is 0 */\n+static void test_dyn_buf_zero_count(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\tconst void __user *kernel_addr = (void __user *)-(unsigned long)PAGE_SIZE;\n+\n+\tinit_dyn_buf_param(\u0026param);\n+\n+\tKUNIT_EXPECT_TRUE(test, validate_dyn_buf(\u0026param, NULL, 0));\n+\tKUNIT_EXPECT_TRUE(test, validate_dyn_buf(\u0026param, (void __user *)PAGE_SIZE, 0));\n+\tKUNIT_EXPECT_TRUE(test, validate_dyn_buf(\u0026param, kernel_addr, 0));\n+}\n+\n+/* Dynamic buffer: NULL is rejected when the size is non-zero */\n+static void test_dyn_buf_null_nonzero_count(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\n+\tinit_dyn_buf_param(\u0026param);\n+\n+\tKUNIT_EXPECT_FALSE(test, validate_dyn_buf(\u0026param, NULL, 1));\n+\tKUNIT_EXPECT_FALSE(test, validate_dyn_buf(\u0026param, NULL, PAGE_SIZE));\n+}\n+\n+/*\n+ * Some architectures (separate user address space, !MMU) accept any address\n+ * in access_ok(), so the rejection cases below only apply where it bounds\n+ * user ranges.\n+ */\n+static bool kapi_rejects_kernel_addr(const void __user *addr)\n+{\n+\treturn !access_ok(addr, 1);\n+}\n+\n+static bool kapi_bounds_user_range(const void __user *base)\n+{\n+\treturn access_ok(base, PAGE_SIZE) \u0026\u0026 !access_ok(base, 3 * PAGE_SIZE);\n+}\n+\n+/* Dynamic buffer: addresses outside user space are rejected when the size is non-zero */\n+static void test_dyn_buf_kernel_addr(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\tconst void __user *kernel_addr = (void __user *)-(unsigned long)PAGE_SIZE;\n+\tconst void __user *top = (void __user *)ULONG_MAX;\n+\n+\tinit_dyn_buf_param(\u0026param);\n+\n+\tif (!kapi_rejects_kernel_addr(kernel_addr) || !kapi_rejects_kernel_addr(top))\n+\t\tkunit_skip(test, \"access_ok() accepts kernel addresses on this architecture\");\n+\n+\tKUNIT_EXPECT_FALSE(test, validate_dyn_buf(\u0026param, kernel_addr, 1));\n+\tKUNIT_EXPECT_FALSE(test, validate_dyn_buf(\u0026param, top, 1));\n+}\n+\n+/* Dynamic buffer: an unset size_multiplier means byte units */\n+static void test_dyn_buf_default_multiplier(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\tconst void __user *near_top = (void __user *)(TASK_SIZE_MAX - 2 * PAGE_SIZE);\n+\n+\tinit_dyn_buf_param(\u0026param);\n+\n+\tKUNIT_EXPECT_TRUE(test, validate_dyn_buf(\u0026param, (void __user *)PAGE_SIZE, 16));\n+\tKUNIT_EXPECT_TRUE(test, validate_dyn_buf(\u0026param, near_top, 0));\n+\n+\tif (!kapi_bounds_user_range(near_top))\n+\t\tkunit_skip(test, \"access_ok() does not bound user ranges on this architecture\");\n+\n+\tKUNIT_EXPECT_TRUE(test, validate_dyn_buf(\u0026param, near_top, PAGE_SIZE));\n+\tKUNIT_EXPECT_FALSE(test, validate_dyn_buf(\u0026param, near_top, 3 * PAGE_SIZE));\n+}\n+\n+/* Dynamic buffer: explicit multiplier scales the size, negative and overflowing counts fail */\n+static void test_dyn_buf_multiplier_and_bad_count(struct kunit *test)\n+{\n+\tstruct kapi_param_spec param = {};\n+\tconst void __user *near_top = (void __user *)(TASK_SIZE_MAX - 2 * PAGE_SIZE);\n+\n+\tinit_dyn_buf_param(\u0026param);\n+\tparam.size_multiplier = 8;\n+\n+\tKUNIT_EXPECT_FALSE(test, validate_dyn_buf(\u0026param, (void __user *)PAGE_SIZE,\n+\t\t\t\t\t\t  (s64)(SIZE_MAX / 8 + 1)));\n+\tKUNIT_EXPECT_FALSE(test, validate_dyn_buf(\u0026param, (void __user *)PAGE_SIZE, -1));\n+\n+\tif (!kapi_bounds_user_range(near_top))\n+\t\tkunit_skip(test, \"access_ok() does not bound user ranges on this architecture\");\n+\n+\tKUNIT_EXPECT_TRUE(test, validate_dyn_buf(\u0026param, near_top, PAGE_SIZE / 8));\n+\tKUNIT_EXPECT_FALSE(test, validate_dyn_buf(\u0026param, near_top, PAGE_SIZE));\n+}\n+\n+#endif /* CONFIG_KAPI_RUNTIME_CHECKS */\n+\n+static const struct kernel_api_spec kapi_test_macro_spec = {\n+\t.name = \"test_macro_spec\",\n+\tKAPI_SIGNAL_MASK_COUNT(1)\n+\tKAPI_SIGNAL_MASK(0, \"blocked\", \"Signals blocked while waiting\")\n+\t\tKAPI_SIGNAL_MASK_SIGNALS(SIGINT, SIGTERM, SIGQUIT)\n+\t},\n+\tKAPI_CAPABILITY_COUNT(1)\n+\tKAPI_CAPABILITY(0, CAP_SYS_ADMIN, \"CAP_SYS_ADMIN\", KAPI_CAP_BYPASS_CHECK)\n+\t\tKAPI_CAP_ALTERNATIVE(CAP_SYS_RESOURCE, CAP_NET_ADMIN)\n+\t},\n+};\n+\n+/* Signal mask and capability alternative macros fill in their counts */\n+static void test_signal_mask_and_cap_alternative(struct kunit *test)\n+{\n+\tconst struct kernel_api_spec *spec = \u0026kapi_test_macro_spec;\n+\tchar *buf;\n+\n+\tKUNIT_EXPECT_EQ(test, spec-\u003esignal_mask_count, 1U);\n+\tKUNIT_EXPECT_EQ(test, spec-\u003esignal_masks[0].signal_count, 3U);\n+\tKUNIT_EXPECT_EQ(test, spec-\u003esignal_masks[0].signals[2], SIGQUIT);\n+\tKUNIT_EXPECT_EQ(test, spec-\u003ecapabilities[0].alternative_count, 2U);\n+\tKUNIT_EXPECT_EQ(test, spec-\u003ecapabilities[0].alternative[1], CAP_NET_ADMIN);\n+\n+\tbuf = kunit_kzalloc(test, 8192, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_NULL(test, buf);\n+\tKUNIT_ASSERT_GT(test, kapi_export_json(spec, buf, 8192), 0);\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"blocked\\\"\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"alternatives\\\"\"));\n+}\n+\n+/* Unregister non-existent spec is a no-op */\n+static void test_unregister_nonexistent(struct kunit *test)\n+{\n+\t/* Should not crash or error */\n+\tkapi_unregister_spec(\"nonexistent_spec_xyz\");\n+}\n+\n+/* Multiple specs can be registered and looked up */\n+static void test_multiple_specs(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec1, *spec2;\n+\tconst struct kernel_api_spec *found;\n+\n+\tspec1 = kzalloc_obj(*spec1, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec1);\n+\tspec2 = kzalloc_obj(*spec2, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec2);\n+\n+\tinit_test_spec(spec1, \"multi_spec_1\");\n+\tinit_test_spec(spec2, \"multi_spec_2\");\n+\n+\tKUNIT_ASSERT_EQ(test, kapi_register_spec(spec1), 0);\n+\tKUNIT_ASSERT_EQ(test, kapi_register_spec(spec2), 0);\n+\n+\tfound = kapi_get_spec(\"multi_spec_1\");\n+\tKUNIT_EXPECT_PTR_EQ(test, found, (const struct kernel_api_spec *)spec1);\n+\n+\tfound = kapi_get_spec(\"multi_spec_2\");\n+\tKUNIT_EXPECT_PTR_EQ(test, found, (const struct kernel_api_spec *)spec2);\n+\n+\tkapi_unregister_spec(\"multi_spec_1\");\n+\tkapi_unregister_spec(\"multi_spec_2\");\n+\tkfree(spec1);\n+\tkfree(spec2);\n+}\n+\n+/* JSON export produces valid output */\n+static void test_json_export(struct kunit *test)\n+{\n+\tstatic const s64 enum_vals[] = { 3, -1 };\n+\tstatic const s64 err_vals[] = { -EINVAL, -EFAULT };\n+\tstruct kernel_api_spec *spec;\n+\tchar *buf;\n+\tint ret;\n+\n+\tspec = kzalloc_obj(*spec, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\n+\tbuf = kzalloc(4096, GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);\n+\n+\tinit_test_spec(spec, \"test_json\");\n+\tspec-\u003eparam_count = 1;\n+\tspec-\u003eparams[0].name = \"arg0\";\n+\tspec-\u003eparams[0].type_name = \"int\";\n+\tspec-\u003eparams[0].constraint_type = KAPI_CONSTRAINT_ENUM;\n+\tspec-\u003eparams[0].enum_values = enum_vals;\n+\tspec-\u003eparams[0].enum_count = ARRAY_SIZE(enum_vals);\n+\tspec-\u003eparams[0].valid_mask = 0xff;\n+\tspec-\u003eparams[0].size_param_idx = 2;\n+\tspec-\u003ereturn_spec.check_type = KAPI_RETURN_ERROR_CHECK;\n+\tspec-\u003ereturn_spec.error_values = err_vals;\n+\tspec-\u003ereturn_spec.error_count = ARRAY_SIZE(err_vals);\n+\n+\tret = kapi_export_json(spec, buf, 4096);\n+\tKUNIT_EXPECT_GT(test, ret, 0);\n+\n+\t/* Verify it starts with '{' and ends with '}' */\n+\tKUNIT_EXPECT_EQ(test, buf[0], '{');\n+\tKUNIT_ASSERT_GT(test, ret, 1);\n+\t/* Find last non-whitespace char */\n+\twhile (ret \u003e 0 \u0026\u0026 (buf[ret - 1] == '\\n' || buf[ret - 1] == ' '))\n+\t\tret--;\n+\tKUNIT_EXPECT_EQ(test, buf[ret - 1], '}');\n+\n+\t/* Verify key fields are present */\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"name\\\"\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"test_json\\\"\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"parameters\\\"\"));\n+\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"constraint_type\\\": \\\"enum\\\"\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"enum_values\\\": [3, -1]\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"valid_mask\\\": \\\"0xff\\\"\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"size_param_idx\\\": 1\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"error_values\\\": [-22, -14]\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"state_transitions\\\"\"));\n+\tKUNIT_EXPECT_NOT_NULL(test, strstr(buf, \"\\\"struct_specs\\\"\"));\n+\n+\tkfree(buf);\n+\tkfree(spec);\n+}\n+\n+/* JSON export with NULL args returns -EINVAL */\n+static void test_json_export_null(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\tchar buf[64];\n+\tint ret;\n+\n+\tret = kapi_export_json(NULL, buf, sizeof(buf));\n+\tKUNIT_EXPECT_EQ(test, ret, -EINVAL);\n+\n+\tspec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\tinit_test_spec(spec, \"test\");\n+\n+\tret = kapi_export_json(spec, NULL, 64);\n+\tKUNIT_EXPECT_EQ(test, ret, -EINVAL);\n+\n+\tret = kapi_export_json(spec, buf, 0);\n+\tKUNIT_EXPECT_EQ(test, ret, -EINVAL);\n+}\n+\n+/* JSON export into a buffer that is too small reports -E2BIG */\n+static void test_json_export_small_buffer(struct kunit *test)\n+{\n+\tstruct kernel_api_spec *spec;\n+\tchar buf[64];\n+\tint ret;\n+\n+\tspec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);\n+\tinit_test_spec(spec, \"test_small\");\n+\n+\tret = kapi_export_json(spec, buf, sizeof(buf));\n+\n+\tKUNIT_EXPECT_EQ(test, ret, -E2BIG);\n+}\n+\n+static struct kunit_case kapi_test_cases[] = {\n+\tKUNIT_CASE(test_register_valid),\n+\tKUNIT_CASE(test_lookup_registered),\n+\tKUNIT_CASE(test_double_register),\n+\tKUNIT_CASE(test_unregister),\n+\tKUNIT_CASE(test_get_spec_null),\n+\tKUNIT_CASE(test_register_null),\n+\tKUNIT_CASE(test_register_null_name),\n+#ifdef CONFIG_KAPI_RUNTIME_CHECKS\n+\tKUNIT_CASE(test_constraint_range_valid),\n+\tKUNIT_CASE(test_constraint_range_invalid),\n+\tKUNIT_CASE(test_constraint_mask_valid),\n+\tKUNIT_CASE(test_constraint_mask_invalid),\n+\tKUNIT_CASE(test_constraint_power_of_two),\n+\tKUNIT_CASE(test_constraint_page_aligned),\n+\tKUNIT_CASE(test_constraint_nonzero),\n+\tKUNIT_CASE(test_return_validation),\n+\tKUNIT_CASE(test_return_known_error),\n+\tKUNIT_CASE(test_return_unknown_error),\n+\tKUNIT_CASE(test_constraint_alignment),\n+\tKUNIT_CASE(test_fd_int_overflow),\n+\tKUNIT_CASE(test_constraint_enum),\n+\tKUNIT_CASE(test_constraint_buffer),\n+\tKUNIT_CASE(test_return_range),\n+\tKUNIT_CASE(test_dyn_buf_zero_count),\n+\tKUNIT_CASE(test_dyn_buf_null_nonzero_count),\n+\tKUNIT_CASE(test_dyn_buf_kernel_addr),\n+\tKUNIT_CASE(test_dyn_buf_default_multiplier),\n+\tKUNIT_CASE(test_dyn_buf_multiplier_and_bad_count),\n+#endif\n+\tKUNIT_CASE(test_signal_mask_and_cap_alternative),\n+\tKUNIT_CASE(test_unregister_nonexistent),\n+\tKUNIT_CASE(test_multiple_specs),\n+\tKUNIT_CASE(test_json_export),\n+\tKUNIT_CASE(test_json_export_null),\n+\tKUNIT_CASE(test_json_export_small_buffer),\n+\t{}\n+};\n+\n+static struct kunit_suite kapi_test_suite = {\n+\t.name = \"kapi\",\n+\t.test_cases = kapi_test_cases,\n+};\n+\n+kunit_test_suite(kapi_test_suite);\n+\n+MODULE_DESCRIPTION(\"KUnit tests for Kernel API Specification Framework\");\n+MODULE_LICENSE(\"GPL\");\ndiff --git a/kernel/api/kernel_api_spec.c b/kernel/api/kernel_api_spec.c\nnew file mode 100644\nindex 0000000000000..993674ce67789\n--- /dev/null\n+++ b/kernel/api/kernel_api_spec.c\n@@ -0,0 +1,1470 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+ *\n+ * kernel_api_spec.c - Kernel API Specification Framework Implementation\n+ *\n+ * Provides runtime support for kernel API specifications including validation,\n+ * export to various formats, and querying capabilities.\n+ */\n+\n+#define pr_fmt(fmt) \"kapi: \" fmt\n+\n+#include \u003clinux/kernel.h\u003e\n+#include \u003clinux/kernel_api_spec.h\u003e\n+#include \u003clinux/string.h\u003e\n+#include \u003clinux/slab.h\u003e\n+#include \u003clinux/list.h\u003e\n+#include \u003clinux/mutex.h\u003e\n+#include \u003clinux/export.h\u003e\n+#include \u003clinux/preempt.h\u003e\n+#include \u003clinux/hardirq.h\u003e\n+#include \u003clinux/file.h\u003e\n+#include \u003clinux/fdtable.h\u003e\n+#include \u003clinux/uaccess.h\u003e\n+#include \u003clinux/limits.h\u003e\n+#include \u003clinux/fcntl.h\u003e\n+#include \u003clinux/mm.h\u003e\n+#include \u003clinux/ratelimit.h\u003e\n+\n+#include \"internal.h\"\n+\n+/* Dynamic API registration */\n+static LIST_HEAD(dynamic_api_specs);\n+static DEFINE_MUTEX(api_spec_mutex);\n+\n+struct dynamic_api_spec {\n+\tstruct list_head list;\n+\tconst struct kernel_api_spec *spec;\n+};\n+\n+/*\n+ * __kapi_find_spec_locked - Internal lookup, caller must hold api_spec_mutex\n+ */\n+static const struct kernel_api_spec *__kapi_find_spec_locked(const char *name)\n+{\n+\tconst struct kernel_api_spec * const *pp;\n+\tstruct dynamic_api_spec *dyn_spec;\n+\n+\tfor (pp = __start_kapi_specs; pp \u003c __stop_kapi_specs; pp++) {\n+\t\tconst struct kernel_api_spec *spec = *pp;\n+\n+\t\tif (spec \u0026\u0026 spec-\u003ename \u0026\u0026 strcmp(spec-\u003ename, name) == 0)\n+\t\t\treturn spec;\n+\t}\n+\n+\tlist_for_each_entry(dyn_spec, \u0026dynamic_api_specs, list) {\n+\t\tif (dyn_spec-\u003espec-\u003ename \u0026\u0026\n+\t\t    strcmp(dyn_spec-\u003espec-\u003ename, name) == 0)\n+\t\t\treturn dyn_spec-\u003espec;\n+\t}\n+\n+\treturn NULL;\n+}\n+\n+/**\n+ * kapi_get_spec - Get API specification by name\n+ * @name: Function name to look up\n+ *\n+ * Return: Pointer to the API specification, or NULL if not found. The\n+ * pointer stays valid for specifications in the ``.kapi_specs`` ELF section\n+ * (built-in, statically defined). A dynamically registered spec stays valid\n+ * only until kapi_unregister_spec() is called for it, so the caller must\n+ * serialize against unregistration.\n+ *\n+ * Context: May sleep. Do not call under spinlock or in IRQ context.\n+ */\n+const struct kernel_api_spec *kapi_get_spec(const char *name)\n+{\n+\tconst struct kernel_api_spec *spec;\n+\n+\tif (!name)\n+\t\treturn NULL;\n+\n+\tmutex_lock(\u0026api_spec_mutex);\n+\tspec = __kapi_find_spec_locked(name);\n+\tmutex_unlock(\u0026api_spec_mutex);\n+\n+\treturn spec;\n+}\n+EXPORT_SYMBOL_GPL(kapi_get_spec);\n+\n+/**\n+ * kapi_register_spec - Register a dynamic API specification\n+ * @spec: API specification to register\n+ *\n+ * Return: 0 on success, negative error code on failure\n+ */\n+int kapi_register_spec(const struct kernel_api_spec *spec)\n+{\n+\tstruct dynamic_api_spec *dyn_spec;\n+\tint ret = 0;\n+\n+\tif (!spec || !spec-\u003ename || !spec-\u003ename[0])\n+\t\treturn -EINVAL;\n+\n+\tdyn_spec = kzalloc_obj(*dyn_spec, GFP_KERNEL);\n+\tif (!dyn_spec)\n+\t\treturn -ENOMEM;\n+\n+\tdyn_spec-\u003espec = spec;\n+\n+\tmutex_lock(\u0026api_spec_mutex);\n+\n+\t/* Check if already exists while holding lock to prevent races */\n+\tif (__kapi_find_spec_locked(spec-\u003ename)) {\n+\t\tret = -EEXIST;\n+\t\tkfree(dyn_spec);\n+\t} else {\n+\t\tlist_add_tail(\u0026dyn_spec-\u003elist, \u0026dynamic_api_specs);\n+\t}\n+\n+\tmutex_unlock(\u0026api_spec_mutex);\n+\n+\treturn ret;\n+}\n+EXPORT_SYMBOL_GPL(kapi_register_spec);\n+\n+/**\n+ * kapi_unregister_spec - Unregister a dynamic API specification\n+ * @name: Name of API to unregister\n+ */\n+void kapi_unregister_spec(const char *name)\n+{\n+\tstruct dynamic_api_spec *dyn_spec, *tmp;\n+\n+\tif (!name)\n+\t\treturn;\n+\n+\tmutex_lock(\u0026api_spec_mutex);\n+\tlist_for_each_entry_safe(dyn_spec, tmp, \u0026dynamic_api_specs, list) {\n+\t\tif (dyn_spec-\u003espec-\u003ename \u0026\u0026\n+\t\t    strcmp(dyn_spec-\u003espec-\u003ename, name) == 0) {\n+\t\t\tlist_del(\u0026dyn_spec-\u003elist);\n+\t\t\tkfree(dyn_spec);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tmutex_unlock(\u0026api_spec_mutex);\n+}\n+EXPORT_SYMBOL_GPL(kapi_unregister_spec);\n+\n+/**\n+ * kapi_param_type_to_string - Convert parameter type to string\n+ * @type: Parameter type\n+ *\n+ * Return: String representation of type\n+ */\n+const char *kapi_param_type_to_string(enum kapi_param_type type)\n+{\n+\tstatic const char * const type_names[] = {\n+\t\t[KAPI_TYPE_VOID] = \"void\",\n+\t\t[KAPI_TYPE_INT] = \"int\",\n+\t\t[KAPI_TYPE_UINT] = \"uint\",\n+\t\t[KAPI_TYPE_PTR] = \"pointer\",\n+\t\t[KAPI_TYPE_STRUCT] = \"struct\",\n+\t\t[KAPI_TYPE_UNION] = \"union\",\n+\t\t[KAPI_TYPE_ENUM] = \"enum\",\n+\t\t[KAPI_TYPE_FUNC_PTR] = \"function_pointer\",\n+\t\t[KAPI_TYPE_ARRAY] = \"array\",\n+\t\t[KAPI_TYPE_FD] = \"file_descriptor\",\n+\t\t[KAPI_TYPE_USER_PTR] = \"user_pointer\",\n+\t\t[KAPI_TYPE_PATH] = \"pathname\",\n+\t\t[KAPI_TYPE_CUSTOM] = \"custom\",\n+\t};\n+\n+\tif (type \u003e= ARRAY_SIZE(type_names))\n+\t\treturn \"unknown\";\n+\n+\treturn type_names[type];\n+}\n+\n+/**\n+ * kapi_lock_type_to_string - Convert lock type to string\n+ * @type: Lock type\n+ *\n+ * Return: String representation of lock type\n+ */\n+const char *kapi_lock_type_to_string(enum kapi_lock_type type)\n+{\n+\tstatic const char * const lock_names[] = {\n+\t\t[KAPI_LOCK_NONE] = \"none\",\n+\t\t[KAPI_LOCK_MUTEX] = \"mutex\",\n+\t\t[KAPI_LOCK_SPINLOCK] = \"spinlock\",\n+\t\t[KAPI_LOCK_RWLOCK] = \"rwlock\",\n+\t\t[KAPI_LOCK_SEQLOCK] = \"seqlock\",\n+\t\t[KAPI_LOCK_RCU] = \"rcu\",\n+\t\t[KAPI_LOCK_SEMAPHORE] = \"semaphore\",\n+\t\t[KAPI_LOCK_CUSTOM] = \"custom\",\n+\t};\n+\n+\tif (type \u003e= ARRAY_SIZE(lock_names))\n+\t\treturn \"unknown\";\n+\n+\treturn lock_names[type];\n+}\n+\n+/**\n+ * kapi_lock_scope_to_string - Convert lock scope to string\n+ * @scope: Lock scope\n+ *\n+ * Return: String representation of lock scope\n+ */\n+const char *kapi_lock_scope_to_string(enum kapi_lock_scope scope)\n+{\n+\tstatic const char * const scope_names[] = {\n+\t\t[KAPI_LOCK_INTERNAL] = \"internal\",\n+\t\t[KAPI_LOCK_ACQUIRES] = \"acquires\",\n+\t\t[KAPI_LOCK_RELEASES] = \"releases\",\n+\t\t[KAPI_LOCK_CALLER_HELD] = \"caller_held\",\n+\t};\n+\n+\tif (scope \u003e= ARRAY_SIZE(scope_names))\n+\t\treturn \"unknown\";\n+\n+\treturn scope_names[scope];\n+}\n+\n+/**\n+ * return_check_type_to_string - Convert return check type to string\n+ * @type: Return check type\n+ *\n+ * Return: String representation of return check type\n+ */\n+static const char *return_check_type_to_string(enum kapi_return_check_type type)\n+{\n+\tstatic const char * const check_names[] = {\n+\t\t[KAPI_RETURN_EXACT] = \"exact\",\n+\t\t[KAPI_RETURN_RANGE] = \"range\",\n+\t\t[KAPI_RETURN_ERROR_CHECK] = \"error_check\",\n+\t\t[KAPI_RETURN_FD] = \"file_descriptor\",\n+\t\t[KAPI_RETURN_CUSTOM] = \"custom\",\n+\t\t[KAPI_RETURN_NO_RETURN] = \"no_return\",\n+\t};\n+\n+\tif (type \u003e= ARRAY_SIZE(check_names))\n+\t\treturn \"unknown\";\n+\n+\treturn check_names[type];\n+}\n+\n+/**\n+ * capability_action_to_string - Convert capability action to string\n+ * @action: Capability action\n+ *\n+ * Return: String representation of capability action\n+ */\n+static const char *capability_action_to_string(enum kapi_capability_action action)\n+{\n+\tstatic const char * const action_names[] = {\n+\t\t[KAPI_CAP_BYPASS_CHECK] = \"bypass_check\",\n+\t\t[KAPI_CAP_INCREASE_LIMIT] = \"increase_limit\",\n+\t\t[KAPI_CAP_OVERRIDE_RESTRICTION] = \"override_restriction\",\n+\t\t[KAPI_CAP_GRANT_PERMISSION] = \"grant_permission\",\n+\t\t[KAPI_CAP_MODIFY_BEHAVIOR] = \"modify_behavior\",\n+\t\t[KAPI_CAP_ACCESS_RESOURCE] = \"access_resource\",\n+\t\t[KAPI_CAP_PERFORM_OPERATION] = \"perform_operation\",\n+\t};\n+\n+\tif (action \u003e= ARRAY_SIZE(action_names))\n+\t\treturn \"unknown\";\n+\n+\treturn action_names[action];\n+}\n+\n+/**\n+ * constraint_type_to_string - Convert constraint type to string\n+ * @type: Constraint type\n+ *\n+ * Return: String representation of constraint type\n+ */\n+static const char *constraint_type_to_string(enum kapi_constraint_type type)\n+{\n+\tstatic const char * const constraint_names[] = {\n+\t\t[KAPI_CONSTRAINT_NONE] = \"none\",\n+\t\t[KAPI_CONSTRAINT_RANGE] = \"range\",\n+\t\t[KAPI_CONSTRAINT_MASK] = \"mask\",\n+\t\t[KAPI_CONSTRAINT_ENUM] = \"enum\",\n+\t\t[KAPI_CONSTRAINT_ALIGNMENT] = \"alignment\",\n+\t\t[KAPI_CONSTRAINT_POWER_OF_TWO] = \"power_of_two\",\n+\t\t[KAPI_CONSTRAINT_PAGE_ALIGNED] = \"page_aligned\",\n+\t\t[KAPI_CONSTRAINT_NONZERO] = \"nonzero\",\n+\t\t[KAPI_CONSTRAINT_USER_STRING] = \"user_string\",\n+\t\t[KAPI_CONSTRAINT_USER_PATH] = \"user_path\",\n+\t\t[KAPI_CONSTRAINT_USER_PTR] = \"user_ptr\",\n+\t\t[KAPI_CONSTRAINT_BUFFER] = \"buffer\",\n+\t\t[KAPI_CONSTRAINT_CUSTOM] = \"custom\",\n+\t};\n+\n+\tif (type \u003e= ARRAY_SIZE(constraint_names))\n+\t\treturn \"unknown\";\n+\n+\treturn constraint_names[type];\n+}\n+\n+/*\n+ * kapi_json_escape - Write a JSON-escaped string into a buffer\n+ * @buf: Output buffer\n+ * @size: Remaining space in buffer\n+ * @str: Input string to escape\n+ *\n+ * Escapes backslash, double-quote, and control characters for JSON output.\n+ * Return: Number of bytes written (via scnprintf semantics)\n+ */\n+static int kapi_json_escape(char *buf, size_t size, const char *str)\n+{\n+\tint ret = 0;\n+\tconst char *p;\n+\n+\tif (!str || size == 0)\n+\t\treturn 0;\n+\n+\tfor (p = str; *p \u0026\u0026 ret \u003c size - 1; p++) {\n+\t\tswitch (*p) {\n+\t\tcase '\\\\':\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"\\\\\\\\\");\n+\t\t\tbreak;\n+\t\tcase '\"':\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"\\\\\\\"\");\n+\t\t\tbreak;\n+\t\tcase '\\n':\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"\\\\n\");\n+\t\t\tbreak;\n+\t\tcase '\\r':\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"\\\\r\");\n+\t\t\tbreak;\n+\t\tcase '\\t':\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"\\\\t\");\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tif ((unsigned char)*p \u003c 0x20) {\n+\t\t\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\t\t\t \"\\\\u%04x\", (unsigned char)*p);\n+\t\t\t} else {\n+\t\t\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\t\t\t \"%c\", *p);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tif (ret \u003c size)\n+\t\tbuf[ret] = '\\0';\n+\n+\treturn ret;\n+}\n+\n+static int kapi_json_str(char *buf, size_t size, const char *str)\n+{\n+\tint ret = 0;\n+\n+\tret += scnprintf(buf, size, \"\\\"\");\n+\tret += kapi_json_escape(buf + ret, size - ret, str);\n+\tret += scnprintf(buf + ret, size - ret, \"\\\"\");\n+\treturn ret;\n+}\n+\n+static int kapi_json_s64_list(char *buf, size_t size, const s64 *vals, u32 count)\n+{\n+\tint ret = scnprintf(buf, size, \"[\");\n+\tu32 i;\n+\n+\tfor (i = 0; vals \u0026\u0026 i \u003c count; i++)\n+\t\tret += scnprintf(buf + ret, size - ret, \"%s%lld\",\n+\t\t\t\t i ? \", \" : \"\", vals[i]);\n+\n+\tret += scnprintf(buf + ret, size - ret, \"]\");\n+\treturn ret;\n+}\n+\n+static int kapi_json_struct_spec(char *buf, size_t size,\n+\t\t\t\t const struct kapi_struct_spec *st)\n+{\n+\tint ret;\n+\tu32 i;\n+\n+\tret = scnprintf(buf, size, \"    {\\n      \\\"name\\\": \");\n+\tret += kapi_json_str(buf + ret, size - ret, st-\u003ename);\n+\tret += scnprintf(buf + ret, size - ret,\n+\t\t\",\\n      \\\"size\\\": %zu,\\n      \\\"alignment\\\": %zu,\\n      \\\"description\\\": \",\n+\t\tst-\u003esize, st-\u003ealignment);\n+\tret += kapi_json_str(buf + ret, size - ret, st-\u003edescription);\n+\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"fields\\\": [\\n\");\n+\n+\tfor (i = 0; i \u003c st-\u003efield_count \u0026\u0026 i \u003c KAPI_MAX_PARAMS; i++) {\n+\t\tconst struct kapi_struct_field *field = \u0026st-\u003efields[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"        {\\n          \\\"name\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, field-\u003ename);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n          \\\"type\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, field-\u003etype_name);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n\"\n+\t\t\t\"          \\\"type_class\\\": \\\"%s\\\",\\n\"\n+\t\t\t\"          \\\"offset\\\": %zu,\\n\"\n+\t\t\t\"          \\\"size\\\": %zu,\\n\"\n+\t\t\t\"          \\\"flags\\\": \\\"0x%x\\\",\\n\"\n+\t\t\t\"          \\\"constraint_type\\\": \\\"%s\\\",\\n\"\n+\t\t\t\"          \\\"min_value\\\": %lld,\\n\"\n+\t\t\t\"          \\\"max_value\\\": %lld,\\n\"\n+\t\t\t\"          \\\"valid_mask\\\": \\\"0x%llx\\\",\\n\"\n+\t\t\t\"          \\\"enum_values\\\": \",\n+\t\t\tkapi_param_type_to_string(field-\u003etype),\n+\t\t\tfield-\u003eoffset, field-\u003esize, field-\u003eflags,\n+\t\t\tconstraint_type_to_string(field-\u003econstraint_type),\n+\t\t\tfield-\u003emin_value, field-\u003emax_value, field-\u003evalid_mask);\n+\t\tret += kapi_json_str(buf + ret, size - ret, field-\u003eenum_values);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n          \\\"description\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, field-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"\\n        }%s\\n\",\n+\t\t\t(i \u003c st-\u003efield_count - 1) ? \",\" : \"\");\n+\t}\n+\n+\tret += scnprintf(buf + ret, size - ret, \"      ]\\n    }\");\n+\treturn ret;\n+}\n+\n+/**\n+ * kapi_export_json - Export API specification to JSON format\n+ * @spec: API specification to export\n+ * @buf: Buffer to write JSON to\n+ * @size: Size of buffer\n+ *\n+ * Return: Number of bytes written, -EINVAL on bad arguments, or -E2BIG\n+ * if the output did not fit in @buf (the contents are then incomplete)\n+ */\n+int kapi_export_json(const struct kernel_api_spec *spec, char *buf, size_t size)\n+{\n+\tint ret = 0;\n+\tint i, j;\n+\n+\tif (!spec || !buf || size == 0)\n+\t\treturn -EINVAL;\n+\n+\tret = scnprintf(buf, size, \"{\\n  \\\"name\\\": \");\n+\tret += kapi_json_str(buf + ret, size - ret, spec-\u003ename);\n+\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t \",\\n  \\\"version\\\": %u,\\n  \\\"description\\\": \",\n+\t\t\t spec-\u003eversion);\n+\tret += kapi_json_str(buf + ret, size - ret, spec-\u003edescription);\n+\tret += scnprintf(buf + ret, size - ret, \",\\n  \\\"long_description\\\": \");\n+\tret += kapi_json_str(buf + ret, size - ret, spec-\u003elong_description);\n+\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t \",\\n  \\\"context_flags\\\": \\\"0x%x\\\",\\n\",\n+\t\t\t spec-\u003econtext_flags);\n+\n+\t/* Parameters */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"parameters\\\": [\\n\");\n+\n+\tfor (i = 0; i \u003c spec-\u003eparam_count \u0026\u0026 i \u003c KAPI_MAX_PARAMS; i++) {\n+\t\tconst struct kapi_param_spec *param = \u0026spec-\u003eparams[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret, \"    {\\n      \\\"name\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, param-\u003ename);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"type\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, param-\u003etype_name);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"type_class\\\": \\\"%s\\\",\\n      \\\"flags\\\": \\\"0x%x\\\",\\n      \\\"description\\\": \",\n+\t\t\tkapi_param_type_to_string(param-\u003etype),\n+\t\t\tparam-\u003eflags);\n+\t\tret += kapi_json_str(buf + ret, size - ret, param-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"constraint_type\\\": \\\"%s\\\",\\n      \\\"constraint_desc\\\": \",\n+\t\t\tconstraint_type_to_string(param-\u003econstraint_type));\n+\t\tret += kapi_json_str(buf + ret, size - ret, param-\u003econstraints);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n\"\n+\t\t\t\"      \\\"min_value\\\": %lld,\\n\"\n+\t\t\t\"      \\\"max_value\\\": %lld,\\n\"\n+\t\t\t\"      \\\"valid_mask\\\": \\\"0x%llx\\\",\\n\"\n+\t\t\t\"      \\\"enum_values\\\": \",\n+\t\t\tparam-\u003emin_value, param-\u003emax_value, param-\u003evalid_mask);\n+\t\tret += kapi_json_s64_list(buf + ret, size - ret,\n+\t\t\t\t\t  param-\u003eenum_values, param-\u003eenum_count);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"size\\\": %zu,\\n      \\\"alignment\\\": %zu,\\n      \\\"size_param_idx\\\": \",\n+\t\t\tparam-\u003esize, param-\u003ealignment);\n+\t\tif (param-\u003esize_param_idx \u003e 0)\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"%d\",\n+\t\t\t\t\t param-\u003esize_param_idx - 1);\n+\t\telse\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"null\");\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"size_multiplier\\\": %zu\\n    }%s\\n\",\n+\t\t\tparam-\u003esize_multiplier,\n+\t\t\t(i \u003c spec-\u003eparam_count - 1) ? \",\" : \"\");\n+\t}\n+\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Return value */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"return\\\": {\\n    \\\"type\\\": \");\n+\tret += kapi_json_str(buf + ret, size - ret, spec-\u003ereturn_spec.type_name);\n+\tret += scnprintf(buf + ret, size - ret,\n+\t\t\",\\n\"\n+\t\t\"    \\\"type_class\\\": \\\"%s\\\",\\n\"\n+\t\t\"    \\\"check_type\\\": \\\"%s\\\",\\n\"\n+\t\t\"    \\\"success_value\\\": %lld,\\n\"\n+\t\t\"    \\\"success_min\\\": %lld,\\n\"\n+\t\t\"    \\\"success_max\\\": %lld,\\n\"\n+\t\t\"    \\\"error_values\\\": \",\n+\t\tkapi_param_type_to_string(spec-\u003ereturn_spec.type),\n+\t\treturn_check_type_to_string(spec-\u003ereturn_spec.check_type),\n+\t\tspec-\u003ereturn_spec.success_value,\n+\t\tspec-\u003ereturn_spec.success_min,\n+\t\tspec-\u003ereturn_spec.success_max);\n+\tret += kapi_json_s64_list(buf + ret, size - ret,\n+\t\t\t\t  spec-\u003ereturn_spec.error_values,\n+\t\t\t\t  spec-\u003ereturn_spec.error_count);\n+\n+\tret += scnprintf(buf + ret, size - ret, \",\\n    \\\"description\\\": \");\n+\tret += kapi_json_str(buf + ret, size - ret, spec-\u003ereturn_spec.description);\n+\tret += scnprintf(buf + ret, size - ret, \"\\n  },\\n\");\n+\n+\t/* Errors */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"errors\\\": [\\n\");\n+\n+\tfor (i = 0; i \u003c spec-\u003eerror_count \u0026\u0026 i \u003c KAPI_MAX_ERRORS; i++) {\n+\t\tconst struct kapi_error_spec *error = \u0026spec-\u003eerrors[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"    {\\n      \\\"code\\\": %d,\\n      \\\"name\\\": \",\n+\t\t\terror-\u003eerror_code);\n+\t\tret += kapi_json_str(buf + ret, size - ret, error-\u003ename);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"condition\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, error-\u003econdition);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"description\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, error-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"\\n    }%s\\n\",\n+\t\t\t(i \u003c spec-\u003eerror_count - 1) ? \",\" : \"\");\n+\t}\n+\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Locks */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"locks\\\": [\\n\");\n+\n+\tfor (i = 0; i \u003c spec-\u003elock_count \u0026\u0026 i \u003c KAPI_MAX_LOCKS; i++) {\n+\t\tconst struct kapi_lock_spec *lock = \u0026spec-\u003elocks[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret, \"    {\\n      \\\"name\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, lock-\u003elock_name);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"type\\\": \\\"%s\\\",\\n      \\\"scope\\\": \\\"%s\\\",\\n      \\\"description\\\": \",\n+\t\t\tkapi_lock_type_to_string(lock-\u003elock_type),\n+\t\t\tkapi_lock_scope_to_string(lock-\u003escope));\n+\t\tret += kapi_json_str(buf + ret, size - ret, lock-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"\\n    }%s\\n\",\n+\t\t\t(i \u003c spec-\u003elock_count - 1) ? \",\" : \"\");\n+\t}\n+\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Capabilities */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"capabilities\\\": [\\n\");\n+\n+\tfor (i = 0; i \u003c spec-\u003ecapability_count \u0026\u0026 i \u003c KAPI_MAX_CAPABILITIES; i++) {\n+\t\tconst struct kapi_capability_spec *cap = \u0026spec-\u003ecapabilities[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"    {\\n      \\\"capability\\\": %d,\\n      \\\"name\\\": \",\n+\t\t\tcap-\u003ecapability);\n+\t\tret += kapi_json_str(buf + ret, size - ret, cap-\u003ecap_name);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"action\\\": \\\"%s\\\",\\n      \\\"allows\\\": \",\n+\t\t\tcapability_action_to_string(cap-\u003eaction));\n+\t\tret += kapi_json_str(buf + ret, size - ret, cap-\u003eallows);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"without_cap\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, cap-\u003ewithout_cap);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"check_condition\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, cap-\u003echeck_condition);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"priority\\\": %u\", cap-\u003epriority);\n+\n+\t\tif (cap-\u003ealternative_count \u003e 0) {\n+\t\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\t\",\\n      \\\"alternatives\\\": [\");\n+\t\t\tfor (j = 0; j \u003c cap-\u003ealternative_count \u0026\u0026 j \u003c KAPI_MAX_CAPABILITIES; j++) {\n+\t\t\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\t\t\"%s%d\", j ? \", \" : \"\",\n+\t\t\t\t\tcap-\u003ealternative[j]);\n+\t\t\t}\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"]\");\n+\t\t}\n+\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"\\n    }%s\\n\",\n+\t\t\t(i \u003c spec-\u003ecapability_count - 1) ? \",\" : \"\");\n+\t}\n+\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Constraints */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"constraints\\\": [\\n\");\n+\tfor (i = 0; i \u003c spec-\u003econstraint_count \u0026\u0026 i \u003c KAPI_MAX_CONSTRAINTS; i++) {\n+\t\tconst struct kapi_constraint_spec *con = \u0026spec-\u003econstraints[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret, \"    {\\n      \\\"name\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, con-\u003ename);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"description\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, con-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"expression\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, con-\u003eexpression);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"\\n    }%s\\n\",\n+\t\t\t(i \u003c spec-\u003econstraint_count - 1) ? \",\" : \"\");\n+\t}\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Signals */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"signals\\\": [\\n\");\n+\tfor (i = 0; i \u003c spec-\u003esignal_count \u0026\u0026 i \u003c KAPI_MAX_SIGNALS; i++) {\n+\t\tconst struct kapi_signal_spec *sig = \u0026spec-\u003esignals[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"    {\\n      \\\"signal_num\\\": %d,\\n      \\\"signal_name\\\": \",\n+\t\t\tsig-\u003esignal_num);\n+\t\tret += kapi_json_str(buf + ret, size - ret, sig-\u003esignal_name);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"direction\\\": \\\"0x%x\\\",\\n      \\\"action\\\": %u,\\n      \\\"target\\\": \",\n+\t\t\tsig-\u003edirection, sig-\u003eaction);\n+\t\tret += kapi_json_str(buf + ret, size - ret, sig-\u003etarget);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"condition\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, sig-\u003econdition);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"description\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, sig-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n\"\n+\t\t\t\"      \\\"restartable\\\": %s,\\n\"\n+\t\t\t\"      \\\"sa_flags_required\\\": \\\"0x%x\\\",\\n\"\n+\t\t\t\"      \\\"sa_flags_forbidden\\\": \\\"0x%x\\\",\\n\"\n+\t\t\t\"      \\\"error_on_signal\\\": %d,\\n\"\n+\t\t\t\"      \\\"transform_to\\\": %d,\\n\"\n+\t\t\t\"      \\\"timing\\\": \",\n+\t\t\tsig-\u003erestartable ? \"true\" : \"false\",\n+\t\t\tsig-\u003esa_flags_required,\n+\t\t\tsig-\u003esa_flags_forbidden,\n+\t\t\tsig-\u003eerror_on_signal,\n+\t\t\tsig-\u003etransform_to);\n+\t\tret += kapi_json_str(buf + ret, size - ret, sig-\u003etiming);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"priority\\\": %u,\\n      \\\"interruptible\\\": %s,\\n      \\\"queue_behavior\\\": \",\n+\t\t\tsig-\u003epriority,\n+\t\t\tsig-\u003einterruptible ? \"true\" : \"false\");\n+\t\tret += kapi_json_str(buf + ret, size - ret, sig-\u003equeue_behavior);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"state_required\\\": \\\"0x%x\\\",\\n      \\\"state_forbidden\\\": \\\"0x%x\\\"\\n    }%s\\n\",\n+\t\t\tsig-\u003estate_required,\n+\t\t\tsig-\u003estate_forbidden,\n+\t\t\t(i \u003c spec-\u003esignal_count - 1) ? \",\" : \"\");\n+\t}\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Side effects */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"side_effects\\\": [\\n\");\n+\tfor (i = 0; i \u003c spec-\u003eside_effect_count \u0026\u0026 i \u003c KAPI_MAX_SIDE_EFFECTS; i++) {\n+\t\tconst struct kapi_side_effect *eff = \u0026spec-\u003eside_effects[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"    {\\n      \\\"type\\\": \\\"0x%x\\\",\\n      \\\"target\\\": \",\n+\t\t\teff-\u003etype);\n+\t\tret += kapi_json_str(buf + ret, size - ret, eff-\u003etarget);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"condition\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, eff-\u003econdition);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"description\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, eff-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\",\\n      \\\"reversible\\\": %s\\n    }%s\\n\",\n+\t\t\teff-\u003ereversible ? \"true\" : \"false\",\n+\t\t\t(i \u003c spec-\u003eside_effect_count - 1) ? \",\" : \"\");\n+\t}\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* State transitions */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"state_transitions\\\": [\\n\");\n+\tfor (i = 0; i \u003c spec-\u003estate_trans_count \u0026\u0026 i \u003c KAPI_MAX_STATE_TRANS; i++) {\n+\t\tconst struct kapi_state_transition *trans = \u0026spec-\u003estate_transitions[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret, \"    {\\n      \\\"object\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, trans-\u003eobject);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"from_state\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, trans-\u003efrom_state);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"to_state\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, trans-\u003eto_state);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"condition\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, trans-\u003econdition);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"description\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, trans-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"\\n    }%s\\n\",\n+\t\t\t(i \u003c spec-\u003estate_trans_count - 1) ? \",\" : \"\");\n+\t}\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Signal masks */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"signal_masks\\\": [\\n\");\n+\tfor (i = 0; i \u003c spec-\u003esignal_mask_count \u0026\u0026 i \u003c KAPI_MAX_SIGNALS; i++) {\n+\t\tconst struct kapi_signal_mask_spec *mask = \u0026spec-\u003esignal_masks[i];\n+\n+\t\tret += scnprintf(buf + ret, size - ret, \"    {\\n      \\\"name\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, mask-\u003emask_name);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"description\\\": \");\n+\t\tret += kapi_json_str(buf + ret, size - ret, mask-\u003edescription);\n+\t\tret += scnprintf(buf + ret, size - ret, \",\\n      \\\"signals\\\": [\");\n+\t\tfor (j = 0; j \u003c mask-\u003esignal_count \u0026\u0026 j \u003c KAPI_MAX_SIGNALS; j++)\n+\t\t\tret += scnprintf(buf + ret, size - ret, \"%s%d\",\n+\t\t\t\t\t j ? \", \" : \"\", mask-\u003esignals[j]);\n+\t\tret += scnprintf(buf + ret, size - ret,\n+\t\t\t\"]\\n    }%s\\n\",\n+\t\t\t(i \u003c spec-\u003esignal_mask_count - 1) ? \",\" : \"\");\n+\t}\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Structure specifications */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"struct_specs\\\": [\\n\");\n+\tfor (i = 0; i \u003c spec-\u003estruct_spec_count \u0026\u0026 i \u003c KAPI_MAX_STRUCT_SPECS; i++) {\n+\t\tret += kapi_json_struct_spec(buf + ret, size - ret,\n+\t\t\t\t\t     \u0026spec-\u003estruct_specs[i]);\n+\t\tret += scnprintf(buf + ret, size - ret, \"%s\\n\",\n+\t\t\t\t (i \u003c spec-\u003estruct_spec_count - 1) ? \",\" : \"\");\n+\t}\n+\tret += scnprintf(buf + ret, size - ret, \"  ],\\n\");\n+\n+\t/* Additional info */\n+\tret += scnprintf(buf + ret, size - ret, \"  \\\"examples\\\": \");\n+\tret += kapi_json_str(buf + ret, size - ret, spec-\u003eexamples);\n+\tret += scnprintf(buf + ret, size - ret, \",\\n  \\\"notes\\\": \");\n+\tret += kapi_json_str(buf + ret, size - ret, spec-\u003enotes);\n+\tret += scnprintf(buf + ret, size - ret, \"\\n}\\n\");\n+\n+\t/* scnprintf() never writes past size - 1, so a full buffer means truncation */\n+\tif (ret \u003e= size - 1)\n+\t\treturn -E2BIG;\n+\n+\treturn ret;\n+}\n+EXPORT_SYMBOL_GPL(kapi_export_json);\n+\n+#ifdef CONFIG_KAPI_RUNTIME_CHECKS\n+\n+#define CREATE_TRACE_POINTS\n+#include \u003ctrace/events/kapi.h\u003e\n+\n+/*\n+ * Render a syscall's parameters as a \"name=value, ...\" string for the\n+ * kapi_syscall_enter tracepoint.  Names come from the spec; pointer-like\n+ * values are shown in hex, integers and file descriptors in decimal.\n+ */\n+static void kapi_trace_format_params(const struct kernel_api_spec *spec,\n+\t\t\t\t     const s64 *args, int nargs,\n+\t\t\t\t     char *buf, size_t size)\n+{\n+\tint i, used = 0;\n+\n+\tbuf[0] = '\\0';\n+\t/* Bound by the caller-supplied arg count; the spec arity may differ. */\n+\tfor (i = 0; args \u0026\u0026 i \u003c nargs \u0026\u0026 i \u003c 6; i++) {\n+\t\tconst char *name = \"arg\";\n+\t\tbool dec = false;\n+\n+\t\tif (i \u003c spec-\u003eparam_count) {\n+\t\t\tconst struct kapi_param_spec *ps = \u0026spec-\u003eparams[i];\n+\n+\t\t\tif (ps-\u003ename)\n+\t\t\t\tname = ps-\u003ename;\n+\t\t\tdec = ps-\u003etype == KAPI_TYPE_INT || ps-\u003etype == KAPI_TYPE_FD;\n+\t\t}\n+\n+\t\tused += scnprintf(buf + used, size - used, \"%s%s=\",\n+\t\t\t\t  i ? \", \" : \"\", name);\n+\t\tif (dec)\n+\t\t\tused += scnprintf(buf + used, size - used, \"%lld\",\n+\t\t\t\t\t  (long long)args[i]);\n+\t\telse\n+\t\t\tused += scnprintf(buf + used, size - used, \"0x%llx\",\n+\t\t\t\t\t  (unsigned long long)args[i]);\n+\t}\n+}\n+\n+/**\n+ * kapi_validate_fd - Validate that a file descriptor value is in valid range\n+ * @fd: File descriptor to validate\n+ *\n+ * Only the numeric range is checked: AT_FDCWD or a non-negative value.\n+ * Openness is left to the syscall, since the fd can be closed between check\n+ * and use. Other negative values are rejected here, so a syscall such as\n+ * close() fails with EINVAL for them rather than EBADF.\n+ *\n+ * Return: true if fd is in valid range, false otherwise\n+ */\n+static bool kapi_validate_fd(int fd)\n+{\n+\treturn fd == AT_FDCWD || fd \u003e= 0;\n+}\n+\n+/**\n+ * kapi_validate_user_ptr - Validate that a user pointer is accessible\n+ * @ptr: User pointer to validate\n+ * @size: Size in bytes to validate\n+ *\n+ * Return: true if user memory is accessible, false otherwise\n+ */\n+static bool kapi_validate_user_ptr(const void __user *ptr, size_t size)\n+{\n+\t/* NULL pointers are not valid; caller handles optional case */\n+\tif (!ptr)\n+\t\treturn false;\n+\n+\treturn access_ok(ptr, size);\n+}\n+\n+/**\n+ * kapi_validate_user_ptr_with_params - Validate user pointer with dynamic size\n+ * @param_spec: Parameter specification\n+ * @ptr: User pointer to validate\n+ * @all_params: Array of all parameter values\n+ * @param_count: Number of parameters\n+ *\n+ * Return: true if user memory is accessible, false otherwise\n+ */\n+static bool kapi_validate_user_ptr_with_params(const struct kapi_param_spec *param_spec,\n+\t\t\t\t\t\tconst void __user *ptr,\n+\t\t\t\t\t\tconst s64 *all_params,\n+\t\t\t\t\t\tint param_count)\n+{\n+\tsize_t actual_size;\n+\n+\t/* NULL is allowed for optional parameters */\n+\tif (!ptr \u0026\u0026 (param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL))\n+\t\treturn true;\n+\n+\t/*\n+\t * size_param_idx is stored 1-based (0 means \"no dynamic sizing\").\n+\t * Convert to a real index before looking into all_params.\n+\t */\n+\tif (param_spec-\u003esize_param_idx \u003e 0 \u0026\u0026\n+\t    param_spec-\u003esize_param_idx - 1 \u003c param_count) {\n+\t\ts64 count = all_params[param_spec-\u003esize_param_idx - 1];\n+\t\tsize_t unit = param_spec-\u003esize_multiplier ?: 1;\n+\n+\t\tif (count \u003c 0) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s: size determinant is negative (%lld)\\n\",\n+\t\t\t\tparam_spec-\u003ename, count);\n+\t\t\treturn false;\n+\t\t}\n+\n+\t\t/* A zero-length access never dereferences the pointer */\n+\t\tif (count == 0)\n+\t\t\treturn true;\n+\n+\t\tif (count \u003e SIZE_MAX / unit) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s: size calculation overflow\\n\",\n+\t\t\t\tparam_spec-\u003ename);\n+\t\t\treturn false;\n+\t\t}\n+\n+\t\tactual_size = (size_t)count * unit;\n+\t} else {\n+\t\tactual_size = param_spec-\u003esize;\n+\t}\n+\n+\treturn kapi_validate_user_ptr(ptr, actual_size);\n+}\n+\n+/**\n+ * kapi_validate_path - Validate that a pathname is accessible and within limits\n+ * @path: User pointer to pathname\n+ * @param_spec: Parameter specification\n+ *\n+ * Return: true if path is valid, false otherwise\n+ */\n+static bool kapi_validate_path(const char __user *path,\n+\t\t\t\tconst struct kapi_param_spec *param_spec)\n+{\n+\tsize_t len;\n+\n+\t/* NULL is allowed for optional parameters */\n+\tif (!path \u0026\u0026 (param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL))\n+\t\treturn true;\n+\n+\tif (!path) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: NULL path not allowed\\n\", param_spec-\u003ename);\n+\t\treturn false;\n+\t}\n+\n+\tif (!access_ok(path, 1)) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: path pointer %p not accessible\\n\",\n+\t\t\tparam_spec-\u003ename, path);\n+\t\treturn false;\n+\t}\n+\n+\t/*\n+\t * Use strnlen_user to check the path length and accessibility.\n+\t * Note: strnlen_user() is subject to TOCTOU -- the measured length\n+\t * may change if another thread modifies the user memory. This is\n+\t * acceptable since the kernel re-copies and re-validates the path\n+\t * later in the syscall path. This check is best-effort.\n+\t */\n+\tlen = strnlen_user(path, PATH_MAX + 1);\n+\tif (len == 0) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: invalid path pointer %p\\n\",\n+\t\t\tparam_spec-\u003ename, path);\n+\t\treturn false;\n+\t}\n+\n+\tif (len \u003e PATH_MAX) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: path too long (exceeds PATH_MAX)\\n\",\n+\t\t\tparam_spec-\u003ename);\n+\t\treturn false;\n+\t}\n+\n+\treturn true;\n+}\n+\n+/**\n+ * kapi_validate_user_string - Validate a userspace null-terminated string\n+ * @str: User pointer to string\n+ * @param_spec: Parameter specification containing length constraints\n+ *\n+ * Validates that the userspace string pointer is accessible and that the\n+ * string length (excluding null terminator) is within the range specified\n+ * by min_value and max_value in the parameter specification.\n+ *\n+ * Return: true if string is valid, false otherwise\n+ */\n+static bool kapi_validate_user_string(const char __user *str,\n+\t\t\t\t       const struct kapi_param_spec *param_spec)\n+{\n+\tsize_t len;\n+\tsize_t max_check_len;\n+\n+\t/* NULL is allowed for optional parameters */\n+\tif (!str \u0026\u0026 (param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL))\n+\t\treturn true;\n+\n+\tif (!str) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: NULL string not allowed\\n\", param_spec-\u003ename);\n+\t\treturn false;\n+\t}\n+\n+\tif (!access_ok(str, 1)) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: string pointer %p not accessible\\n\",\n+\t\t\tparam_spec-\u003ename, str);\n+\t\treturn false;\n+\t}\n+\n+\t/*\n+\t * Use strnlen_user to check the string length and validate accessibility.\n+\t * Check up to max_value + 1 to detect strings that are too long.\n+\t * If max_value is 0 or unset, use PATH_MAX as a reasonable default.\n+\t *\n+\t * Note: strnlen_user() is subject to TOCTOU -- see comment in\n+\t * kapi_validate_path() above. This check is best-effort.\n+\t */\n+\tmax_check_len = param_spec-\u003emax_value \u003e 0 ?\n+\t\t\t(size_t)param_spec-\u003emax_value + 1 : PATH_MAX + 1;\n+\tlen = strnlen_user(str, max_check_len);\n+\n+\tif (len == 0) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: invalid string pointer %p\\n\",\n+\t\t\tparam_spec-\u003ename, str);\n+\t\treturn false;\n+\t}\n+\n+\t/*\n+\t * strnlen_user returns the length including the null terminator.\n+\t * Convert to string length (excluding terminator) for range check.\n+\t */\n+\tlen--;\n+\n+\tif (param_spec-\u003emin_value \u003e 0 \u0026\u0026 len \u003c (size_t)param_spec-\u003emin_value) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: string too short (%zu \u003c %lld)\\n\",\n+\t\t\tparam_spec-\u003ename, len, param_spec-\u003emin_value);\n+\t\treturn false;\n+\t}\n+\n+\tif (param_spec-\u003emax_value \u003e 0 \u0026\u0026 len \u003e (size_t)param_spec-\u003emax_value) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: string too long (%zu \u003e %lld)\\n\",\n+\t\t\tparam_spec-\u003ename, len, param_spec-\u003emax_value);\n+\t\treturn false;\n+\t}\n+\n+\treturn true;\n+}\n+\n+/**\n+ * kapi_validate_user_ptr_constraint - Validate a userspace pointer with size\n+ * @ptr: User pointer to validate\n+ * @param_spec: Parameter specification containing size\n+ *\n+ * Validates that the userspace pointer is accessible and that the memory\n+ * region of the specified size can be accessed. The size is taken from\n+ * the param_spec-\u003esize field.\n+ *\n+ * Return: true if pointer is valid, false otherwise\n+ */\n+static bool kapi_validate_user_ptr_constraint(const void __user *ptr,\n+\t\t\t\t\t       const struct kapi_param_spec *param_spec)\n+{\n+\t/* NULL is allowed for optional parameters */\n+\tif (!ptr \u0026\u0026 (param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL))\n+\t\treturn true;\n+\n+\tif (!ptr) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: NULL pointer not allowed\\n\", param_spec-\u003ename);\n+\t\treturn false;\n+\t}\n+\n+\tif (param_spec-\u003esize == 0) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: size not specified for user pointer validation\\n\",\n+\t\t\tparam_spec-\u003ename);\n+\t\treturn false;\n+\t}\n+\n+\tif (!access_ok(ptr, param_spec-\u003esize)) {\n+\t\tpr_warn_ratelimited(\"Parameter %s: user pointer %p not accessible for %zu bytes\\n\",\n+\t\t\tparam_spec-\u003ename, ptr, param_spec-\u003esize);\n+\t\treturn false;\n+\t}\n+\n+\treturn true;\n+}\n+\n+/*\n+ * check_user_ptr is false once the pointer was validated against its dynamic\n+ * size; the fixed-size check would reject NULL even when that size is 0.\n+ */\n+static bool kapi_validate_param_checks(const struct kapi_param_spec *param_spec,\n+\t\t\t\t       s64 value, bool check_user_ptr)\n+{\n+\tint i;\n+\n+\t/* Special handling for file descriptor type */\n+\tif (param_spec-\u003etype == KAPI_TYPE_FD \u0026\u0026\n+\t    !(param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL)) {\n+\t\tif (value \u003c INT_MIN || value \u003e INT_MAX) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s: file descriptor %lld out of int range\\n\",\n+\t\t\t\tparam_spec-\u003ename, value);\n+\t\t\treturn false;\n+\t\t}\n+\t\tif (!kapi_validate_fd((int)value)) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s: invalid file descriptor %lld\\n\",\n+\t\t\t\tparam_spec-\u003ename, value);\n+\t\t\treturn false;\n+\t\t}\n+\t}\n+\n+\t/* Special handling for user pointer type */\n+\tif (check_user_ptr \u0026\u0026 param_spec-\u003etype == KAPI_TYPE_USER_PTR) {\n+\t\tconst void __user *ptr = (const void __user *)(unsigned long)value;\n+\n+\t\t/* NULL is allowed for optional parameters */\n+\t\tif (!ptr \u0026\u0026 (param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL))\n+\t\t\treturn true;\n+\n+\t\tif (!kapi_validate_user_ptr(ptr, param_spec-\u003esize)) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s: invalid user pointer %p (size: %zu)\\n\",\n+\t\t\t\tparam_spec-\u003ename, ptr, param_spec-\u003esize);\n+\t\t\treturn false;\n+\t\t}\n+\t}\n+\n+\t/* Special handling for path type */\n+\tif (param_spec-\u003etype == KAPI_TYPE_PATH) {\n+\t\tconst char __user *path = (const char __user *)(unsigned long)value;\n+\n+\t\tif (!kapi_validate_path(path, param_spec))\n+\t\t\treturn false;\n+\t}\n+\n+\tswitch (param_spec-\u003econstraint_type) {\n+\tcase KAPI_CONSTRAINT_NONE:\n+\tcase KAPI_CONSTRAINT_BUFFER:\n+\t\treturn true;\n+\n+\tcase KAPI_CONSTRAINT_RANGE:\n+\t\t/*\n+\t\t * If max_value is below min_value, it was likely set from an\n+\t\t * unsigned constant (e.g. SIZE_MAX) that overflowed s64.  Treat\n+\t\t * as no upper bound; only check the minimum.\n+\t\t */\n+\t\tif (param_spec-\u003emax_value \u003e= param_spec-\u003emin_value) {\n+\t\t\tif (value \u003c param_spec-\u003emin_value ||\n+\t\t\t    value \u003e param_spec-\u003emax_value) {\n+\t\t\t\tpr_warn_ratelimited(\"Parameter %s value %lld out of range [%lld, %lld]\\n\",\n+\t\t\t\t\tparam_spec-\u003ename, value,\n+\t\t\t\t\tparam_spec-\u003emin_value,\n+\t\t\t\t\tparam_spec-\u003emax_value);\n+\t\t\t\treturn false;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif (value \u003c param_spec-\u003emin_value) {\n+\t\t\t\tpr_warn_ratelimited(\"Parameter %s value %lld below minimum %lld\\n\",\n+\t\t\t\t\tparam_spec-\u003ename, value,\n+\t\t\t\t\tparam_spec-\u003emin_value);\n+\t\t\t\treturn false;\n+\t\t\t}\n+\t\t}\n+\t\treturn true;\n+\n+\tcase KAPI_CONSTRAINT_MASK:\n+\t\tif (value \u0026 ~param_spec-\u003evalid_mask) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s value 0x%llx contains invalid bits (valid mask: 0x%llx)\\n\",\n+\t\t\t\tparam_spec-\u003ename, value, param_spec-\u003evalid_mask);\n+\t\t\treturn false;\n+\t\t}\n+\t\treturn true;\n+\n+\tcase KAPI_CONSTRAINT_ENUM:\n+\t\tif (!param_spec-\u003eenum_values || param_spec-\u003eenum_count == 0)\n+\t\t\treturn true;\n+\n+\t\tfor (i = 0; i \u003c param_spec-\u003eenum_count; i++) {\n+\t\t\tif (value == param_spec-\u003eenum_values[i])\n+\t\t\t\treturn true;\n+\t\t}\n+\t\tpr_warn_ratelimited(\"Parameter %s value %lld not in valid enumeration\\n\",\n+\t\t\tparam_spec-\u003ename, value);\n+\t\treturn false;\n+\n+\tcase KAPI_CONSTRAINT_ALIGNMENT:\n+\t\tif (param_spec-\u003ealignment == 0) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s: alignment constraint specified but alignment is 0\\n\",\n+\t\t\t\tparam_spec-\u003ename);\n+\t\t\treturn false;\n+\t\t}\n+\t\tif (param_spec-\u003ealignment \u0026 (param_spec-\u003ealignment - 1)) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s: alignment %zu is not a power of two\\n\",\n+\t\t\t\tparam_spec-\u003ename, param_spec-\u003ealignment);\n+\t\t\treturn false;\n+\t\t}\n+\t\tif (value \u0026 (param_spec-\u003ealignment - 1)) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s value 0x%llx not aligned to %zu boundary\\n\",\n+\t\t\t\tparam_spec-\u003ename, value, param_spec-\u003ealignment);\n+\t\t\treturn false;\n+\t\t}\n+\t\treturn true;\n+\n+\tcase KAPI_CONSTRAINT_POWER_OF_TWO:\n+\t\tif (value == 0 || (value \u0026 (value - 1))) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s value %lld is not a power of two\\n\",\n+\t\t\t\tparam_spec-\u003ename, value);\n+\t\t\treturn false;\n+\t\t}\n+\t\treturn true;\n+\n+\tcase KAPI_CONSTRAINT_PAGE_ALIGNED:\n+\t\tif (value \u0026 (PAGE_SIZE - 1)) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s value 0x%llx not page-aligned (PAGE_SIZE=%ld)\\n\",\n+\t\t\t\tparam_spec-\u003ename, value, PAGE_SIZE);\n+\t\t\treturn false;\n+\t\t}\n+\t\treturn true;\n+\n+\tcase KAPI_CONSTRAINT_NONZERO:\n+\t\tif (value == 0) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s must be non-zero\\n\", param_spec-\u003ename);\n+\t\t\treturn false;\n+\t\t}\n+\t\treturn true;\n+\n+\tcase KAPI_CONSTRAINT_USER_STRING:\n+\t\treturn kapi_validate_user_string((const char __user *)(unsigned long)value,\n+\t\t\t\t\t\t param_spec);\n+\n+\tcase KAPI_CONSTRAINT_USER_PATH:\n+\t\treturn kapi_validate_path((const char __user *)(unsigned long)value, param_spec);\n+\n+\tcase KAPI_CONSTRAINT_USER_PTR:\n+\t\treturn kapi_validate_user_ptr_constraint((const void __user *)(unsigned long)value,\n+\t\t\t\t\t\t\t param_spec);\n+\n+\tcase KAPI_CONSTRAINT_CUSTOM:\n+\t\tif (param_spec-\u003evalidate)\n+\t\t\treturn param_spec-\u003evalidate(value);\n+\t\treturn true;\n+\n+\tdefault:\n+\t\treturn true;\n+\t}\n+}\n+\n+/**\n+ * kapi_validate_param - Validate a parameter against its specification\n+ * @param_spec: Parameter specification\n+ * @value: Parameter value to validate\n+ *\n+ * Return: true if valid, false otherwise\n+ */\n+bool kapi_validate_param(const struct kapi_param_spec *param_spec, s64 value)\n+{\n+\treturn kapi_validate_param_checks(param_spec, value, true);\n+}\n+EXPORT_SYMBOL_GPL(kapi_validate_param);\n+\n+/**\n+ * kapi_validate_param_with_context - Validate parameter with access to all params\n+ * @param_spec: Parameter specification\n+ * @value: Parameter value to validate\n+ * @all_params: Array of all parameter values\n+ * @param_count: Number of parameters\n+ *\n+ * Return: true if valid, false otherwise\n+ */\n+bool kapi_validate_param_with_context(const struct kapi_param_spec *param_spec,\n+\t\t\t\t       s64 value, const s64 *all_params, int param_count)\n+{\n+\t/* Special handling for user pointer type with dynamic sizing */\n+\tif (param_spec-\u003etype == KAPI_TYPE_USER_PTR) {\n+\t\tconst void __user *ptr = (const void __user *)(unsigned long)value;\n+\n+\t\t/* NULL is allowed for optional parameters */\n+\t\tif (!ptr \u0026\u0026 (param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL))\n+\t\t\treturn true;\n+\n+\t\tif (!kapi_validate_user_ptr_with_params(param_spec, ptr, all_params, param_count)) {\n+\t\t\tpr_warn_ratelimited(\"Parameter %s: invalid user pointer %p\\n\",\n+\t\t\t\tparam_spec-\u003ename, ptr);\n+\t\t\treturn false;\n+\t\t}\n+\t\treturn kapi_validate_param_checks(param_spec, value, false);\n+\t}\n+\n+\t/* For other types, fall back to regular validation */\n+\treturn kapi_validate_param(param_spec, value);\n+}\n+EXPORT_SYMBOL_GPL(kapi_validate_param_with_context);\n+\n+/**\n+ * kapi_validate_syscall_params - Validate all syscall parameters together\n+ * @spec: API specification\n+ * @params: Array of parameter values\n+ * @param_count: Number of parameters\n+ *\n+ * Return: -EINVAL if any parameter is invalid, 0 if all valid\n+ */\n+int kapi_validate_syscall_params(const struct kernel_api_spec *spec,\n+\t\t\t\t const s64 *params, int param_count)\n+{\n+\tint i, ret = 0;\n+\n+\tif (!spec || !params)\n+\t\treturn 0;\n+\n+\tif (trace_kapi_syscall_enter_enabled()) {\n+\t\tchar pbuf[KAPI_TP_PARAMS_LEN];\n+\n+\t\tkapi_trace_format_params(spec, params, param_count, pbuf, sizeof(pbuf));\n+\t\ttrace_kapi_syscall_enter(spec-\u003ename, param_count, params, pbuf);\n+\t}\n+\n+\t/* Validate that we have the expected number of parameters */\n+\tif (param_count != spec-\u003eparam_count) {\n+\t\tpr_warn_ratelimited(\"API %s: parameter count mismatch (expected %u, got %d)\\n\",\n+\t\t\tspec-\u003ename, spec-\u003eparam_count, param_count);\n+\t\tret = -EINVAL;\n+\t\tgoto out;\n+\t}\n+\n+\t/* Validate each parameter with context */\n+\tfor (i = 0; i \u003c spec-\u003eparam_count \u0026\u0026 i \u003c KAPI_MAX_PARAMS; i++) {\n+\t\tconst struct kapi_param_spec *param_spec = \u0026spec-\u003eparams[i];\n+\n+\t\tif (!kapi_validate_param_with_context(param_spec, params[i], params, param_count)) {\n+\t\t\tif (strncmp(spec-\u003ename, \"sys_\", 4) == 0) {\n+\t\t\t\t/* For syscalls, we can return EINVAL to userspace */\n+\t\t\t\tret = -EINVAL;\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+out:\n+\t/*\n+\t * Emit the exit event on the rejection path too (the wrapper\n+\t * short-circuits the handler on a non-zero return), so every\n+\t * kapi_syscall_enter has a matching kapi_syscall_exit.\n+\t */\n+\tif (ret)\n+\t\ttrace_kapi_syscall_exit(spec-\u003ename, ret, false);\n+\n+\treturn ret;\n+}\n+EXPORT_SYMBOL_GPL(kapi_validate_syscall_params);\n+\n+/**\n+ * kapi_check_return_success - Check if return value indicates success\n+ * @return_spec: Return specification\n+ * @retval: Return value to check\n+ *\n+ * Return: true if the return value indicates success according to the spec.\n+ */\n+bool kapi_check_return_success(const struct kapi_return_spec *return_spec, s64 retval)\n+{\n+\tu32 i;\n+\n+\tif (!return_spec)\n+\t\treturn true;\n+\n+\tswitch (return_spec-\u003echeck_type) {\n+\tcase KAPI_RETURN_EXACT:\n+\t\treturn retval == return_spec-\u003esuccess_value;\n+\n+\tcase KAPI_RETURN_RANGE:\n+\t\treturn retval \u003e= return_spec-\u003esuccess_min \u0026\u0026\n+\t\t       retval \u003c= return_spec-\u003esuccess_max;\n+\n+\tcase KAPI_RETURN_ERROR_CHECK:\n+\t\t/* Success if NOT in error list */\n+\t\tif (return_spec-\u003eerror_values) {\n+\t\t\tfor (i = 0; i \u003c return_spec-\u003eerror_count; i++) {\n+\t\t\t\tif (retval == return_spec-\u003eerror_values[i])\n+\t\t\t\t\treturn false;\n+\t\t\t}\n+\t\t}\n+\t\treturn true;\n+\n+\tcase KAPI_RETURN_FD:\n+\t\t/* File descriptors: \u003e= 0 is success, \u003c 0 is error */\n+\t\treturn retval \u003e= 0;\n+\n+\tcase KAPI_RETURN_CUSTOM:\n+\t\tif (return_spec-\u003eis_success)\n+\t\t\treturn return_spec-\u003eis_success(retval);\n+\t\tfallthrough;\n+\n+\tdefault:\n+\t\treturn true;\n+\t}\n+}\n+EXPORT_SYMBOL_GPL(kapi_check_return_success);\n+\n+/**\n+ * kapi_validate_return_value - Validate that return value matches spec\n+ * @spec: API specification\n+ * @retval: Return value to validate\n+ *\n+ * Return: false if the spec is a KAPI_RETURN_FD check and a successful @retval\n+ * is not a valid file descriptor, true otherwise.\n+ *\n+ * kapi_check_return_success() runs first. A value that does not satisfy it is\n+ * treated as an error and is accepted. An error code that is not listed in the\n+ * spec is only logged with pr_debug().\n+ */\n+bool kapi_validate_return_value(const struct kernel_api_spec *spec, s64 retval)\n+{\n+\tint i;\n+\tbool is_success;\n+\n+\tif (!spec)\n+\t\treturn true; /* No spec means we can't validate */\n+\n+\t/* First check if this is a success return */\n+\tis_success = kapi_check_return_success(\u0026spec-\u003ereturn_spec, retval);\n+\n+\tif (is_success) {\n+\t\t/* Special validation for file descriptor returns */\n+\t\tif (spec-\u003ereturn_spec.check_type == KAPI_RETURN_FD) {\n+\t\t\tif (retval \u003e INT_MAX || !kapi_validate_fd((int)retval)) {\n+\t\t\t\tpr_warn_ratelimited(\"API %s returned invalid file descriptor %lld\\n\",\n+\t\t\t\t\tspec-\u003ename, retval);\n+\t\t\t\treturn false;\n+\t\t\t}\n+\t\t}\n+\t\treturn true;\n+\t}\n+\n+\tif (spec-\u003eerror_count == 0) {\n+\t\tpr_debug(\"API %s returned unspecified error %lld\\n\",\n+\t\t\t spec-\u003ename, retval);\n+\t\treturn true;\n+\t}\n+\n+\tfor (i = 0; i \u003c spec-\u003eerror_count \u0026\u0026 i \u003c KAPI_MAX_ERRORS; i++) {\n+\t\tif (retval == spec-\u003eerrors[i].error_code)\n+\t\t\treturn true;\n+\t}\n+\n+\t/*\n+\t * Error not in spec - log at debug level since filesystem-specific and\n+\t * device-specific error codes may not be exhaustively listed.\n+\t */\n+\tpr_debug(\"API %s returned error code %lld not listed in spec\\n\",\n+\t\t spec-\u003ename, retval);\n+\n+\treturn true;\n+}\n+EXPORT_SYMBOL_GPL(kapi_validate_return_value);\n+\n+/**\n+ * kapi_validate_syscall_return - Validate syscall return value\n+ * @spec: API specification\n+ * @retval: Return value\n+ *\n+ * Return: always 0. A return value that does not match the spec is only\n+ * logged and the syscall result is left unchanged.\n+ */\n+int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 retval)\n+{\n+\tbool valid = true;\n+\n+\tif (!spec)\n+\t\treturn 0;\n+\n+\t/* Validate against the return spec when one was defined */\n+\tif (spec-\u003ereturn_magic == KAPI_MAGIC_RETURN)\n+\t\tvalid = kapi_validate_return_value(spec, retval);\n+\n+\ttrace_kapi_syscall_exit(spec-\u003ename, retval, valid);\n+\n+\treturn 0;\n+}\n+EXPORT_SYMBOL_GPL(kapi_validate_syscall_return);\n+\n+/**\n+ * kapi_check_context - Check if current context matches API requirements\n+ * @spec: API specification to check against\n+ */\n+void kapi_check_context(const struct kernel_api_spec *spec)\n+{\n+\tbool valid = false;\n+\tu32 ctx;\n+\n+\tif (!spec)\n+\t\treturn;\n+\n+\tctx = spec-\u003econtext_flags;\n+\n+\tif (!ctx)\n+\t\treturn;\n+\n+\t/* Check if we're in an allowed context */\n+\tif ((ctx \u0026 KAPI_CTX_PROCESS) \u0026\u0026 !in_interrupt())\n+\t\tvalid = true;\n+\n+\tif ((ctx \u0026 KAPI_CTX_SOFTIRQ) \u0026\u0026 in_softirq())\n+\t\tvalid = true;\n+\n+\tif ((ctx \u0026 KAPI_CTX_HARDIRQ) \u0026\u0026 in_hardirq())\n+\t\tvalid = true;\n+\n+\tif ((ctx \u0026 KAPI_CTX_NMI) \u0026\u0026 in_nmi())\n+\t\tvalid = true;\n+\n+\tif (!valid)\n+\t\tWARN_ONCE(1, \"API %s called from invalid context\\n\", spec-\u003ename);\n+\n+\t/* Check specific requirements */\n+\tif ((ctx \u0026 KAPI_CTX_ATOMIC) \u0026\u0026 preemptible())\n+\t\tWARN_ONCE(1, \"API %s requires atomic context\\n\", spec-\u003ename);\n+\n+\tif ((ctx \u0026 KAPI_CTX_SLEEPABLE) \u0026\u0026 !preemptible())\n+\t\tWARN_ONCE(1, \"API %s requires sleepable context\\n\", spec-\u003ename);\n+}\n+EXPORT_SYMBOL_GPL(kapi_check_context);\n+\n+#endif /* CONFIG_KAPI_RUNTIME_CHECKS */\ndiff --git a/mm/madvise.c b/mm/madvise.c\nindex eeee82cf2b3f4..4eb29922a2150 100644\n--- a/mm/madvise.c\n+++ b/mm/madvise.c\n@@ -2018,6 +2018,555 @@ int do_madvise(struct mm_struct *mm, unsigned long start, size_t len_in, int beh\n \treturn error;\n }\n \n+/**\n+ * sys_madvise - Give advice about use of memory\n+ * @start: Starting virtual address of the range to advise on\n+ * @len_in: Length of the range in bytes\n+ * @behavior: Advice (a MADV_* constant) the kernel should apply to the range\n+ *\n+ * long-desc: Provides the kernel with advice or directions about the address\n+ *   range starting at start and extending for len_in bytes. The advice is\n+ *   selected by behavior, which is one of the MADV_* constants defined in\n+ *   \u003csys/mman.h\u003e. The behaviors fall into three groups. The hint group\n+ *   updates VMA flags (MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL,\n+ *   MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP, MADV_WIPEONFORK,\n+ *   MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE, MADV_HUGEPAGE,\n+ *   MADV_NOHUGEPAGE). The immediate-action group performs work synchronously\n+ *   while preserving page contents (MADV_WILLNEED, MADV_COLD, MADV_PAGEOUT,\n+ *   MADV_POPULATE_READ, MADV_POPULATE_WRITE, MADV_COLLAPSE, MADV_GUARD_REMOVE,\n+ *   MADV_SOFT_OFFLINE). The destructive group discards, replaces or\n+ *   invalidates page contents (MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE,\n+ *   MADV_REMOVE, MADV_GUARD_INSTALL, MADV_HWPOISON). MADV_GUARD_INSTALL\n+ *   belongs to the destructive group because it zaps any existing pages in\n+ *   the range before installing PTE guard markers.\n+ *\n+ *   start must be page-aligned; len_in is rounded up to the next page\n+ *   boundary internally. Once those validation checks pass, a zero-length\n+ *   range succeeds without performing work. The kernel rejects ranges that\n+ *   wrap (start + PAGE_ALIGN(len_in) \u003c start) and ranges where len_in is\n+ *   non-zero but rounds up to zero. Address tagging bits are stripped\n+ *   from start before VMA lookup for every behavior except MADV_HWPOISON\n+ *   and MADV_SOFT_OFFLINE, which receive the raw start value because they\n+ *   bypass the VMA walk entirely.\n+ *\n+ *   The kernel return value reports whether any error condition was\n+ *   encountered, not whether the requested work was performed. The\n+ *   relationship between the return code and the work done varies by\n+ *   handler:\n+ *\n+ *     - Hint behaviors set or clear VMA flags; how the flag is used later\n+ *       depends on the behavior. MADV_DONTFORK / MADV_DOFORK set or clear\n+ *       VM_DONTCOPY, which dup_mmap() honors, so the child does not get the\n+ *       mapping. MADV_MERGEABLE / MADV_UNMERGEABLE control whether KSM scans\n+ *       the VMA. MADV_NOHUGEPAGE blocks the fault-time, MADV_COLLAPSE and\n+ *       khugepaged THP paths for the VMA. MADV_HUGEPAGE makes the VMA\n+ *       eligible for THP when the transparent_hugepage mode is \"madvise\" and\n+ *       increases defrag effort; it does not force allocation, which still\n+ *       depends on the global mode, VMA suitability, defrag GFP policy, and\n+ *       allocation or memcg-charge success. MADV_WIPEONFORK does not wipe\n+ *       pages at fork time; the child VMA's pages are not copied, and the\n+ *       child sees zero-filled pages when it first touches them.\n+ *       MADV_KEEPONFORK clears VM_WIPEONFORK. MADV_DONTDUMP / MADV_DODUMP set\n+ *       or clear VM_DONTDUMP, although always_dump_vma() still includes gate,\n+ *       vm_ops-named or arch-named VMAs in a core dump. MADV_NORMAL /\n+ *       MADV_RANDOM / MADV_SEQUENTIAL set or clear VM_RAND_READ / VM_SEQ_READ,\n+ *       which only steer read-ahead.\n+ *\n+ *     - Walk-and-skip handlers (MADV_COLD, MADV_PAGEOUT, MADV_FREE,\n+ *       MADV_GUARD_REMOVE) traverse the range and silently skip pages or\n+ *       PMDs that fail per-page preconditions (absent, special, device,\n+ *       shared, non-LRU, unsplittable, locked, etc.), returning 0 even\n+ *       when most or all pages were skipped.\n+ *\n+ *     - Bulk-backend handlers delegate the requested range to a single\n+ *       backend call: MADV_DONTNEED and MADV_DONTNEED_LOCKED to\n+ *       zap_vma_range_batched(), MADV_REMOVE to vfs_fallocate(),\n+ *       MADV_WILLNEED on regular files to vfs_fadvise(). The backend's\n+ *       return is propagated for MADV_REMOVE and discarded for\n+ *       MADV_WILLNEED; DAX files short-circuit MADV_WILLNEED entirely.\n+ *\n+ *     - Stop-on-error handlers (MADV_POPULATE_READ, MADV_POPULATE_WRITE,\n+ *       MADV_SOFT_OFFLINE) walk the range but surface the first per-page\n+ *       failure as an errno (-EHWPOISON, -EFAULT, -ENOMEM, ...) rather\n+ *       than skipping silently.\n+ *\n+ *     - Hybrid handlers combine modes: MADV_WILLNEED walks for anonymous\n+ *       and shmem ranges but bulk-calls vfs_fadvise() for regular files;\n+ *       MADV_COLLAPSE walks PMD-by-PMD and tracks the last scan failure\n+ *       so transient skips coexist with terminal errors;\n+ *       MADV_GUARD_INSTALL walks to install markers and re-walks after\n+ *       zap_vma_range() to clear pre-existing pages, retrying up to\n+ *       MAX_MADVISE_GUARD_RETRIES; MADV_HWPOISON walks pages but folds\n+ *       memory_failure()'s -EOPNOTSUPP back to 0.\n+ *\n+ *   Applications that need to know whether a specific page was acted on\n+ *   must verify the result through other means (e.g. /proc/[pid]/smaps,\n+ *   page faults, read-after-write).\n+ *\n+ *   On success, madvise() returns 0; unlike read(2) and write(2) it has no\n+ *   notion of partial completion at the syscall boundary. When the range\n+ *   spans multiple VMAs, the kernel applies the advice to each in turn. For\n+ *   the behaviors that walk VMAs (all except MADV_POPULATE_*, MADV_HWPOISON\n+ *   and MADV_SOFT_OFFLINE), an unmapped gap inside the range causes the call\n+ *   to return -ENOMEM after processing the mapped portions, rather than\n+ *   aborting at the gap, although the walk stops at the first per-VMA error.\n+ *   MADV_POPULATE_* stops at the first failure, and MADV_HWPOISON and\n+ *   MADV_SOFT_OFFLINE bypass the walk, so an unmapped address gives -EFAULT.\n+ *\n+ *   POSIX defines posix_madvise(3) for a portable subset (POSIX_MADV_NORMAL,\n+ *   _RANDOM, _SEQUENTIAL, _WILLNEED, _DONTNEED). Linux MADV_DONTNEED is\n+ *   destructive: it discards the contents of the affected anonymous pages and\n+ *   subsequent reads return zero. POSIX permits but does not require\n+ *   destruction, so portable code that needs the POSIX semantics should use\n+ *   posix_madvise(3) instead.\n+ *\n+ * contexts: process, sleepable\n+ *\n+ * param: start\n+ *   type: uint, input\n+ *   constraint-type: page_aligned\n+ *   cdesc: Starting virtual address of the range. Must be aligned to\n+ *     PAGE_SIZE. An unaligned start always returns -EINVAL, even when\n+ *     len_in is zero. Address tag bits, where supported by the architecture,\n+ *     are cleared via untagged_addr() before the range is interpreted, with\n+ *     the exception of MADV_HWPOISON and MADV_SOFT_OFFLINE, which receive\n+ *     the raw start value because they bypass the VMA walk.\n+ *\n+ * param: len_in\n+ *   type: uint, input\n+ *   cdesc: Length of the range in bytes. Internally rounded up to a multiple\n+ *     of PAGE_SIZE. A len_in of 0 is accepted and the call is a no-op that\n+ *     returns 0. A non-zero len_in that rounds up to 0 (i.e. wraps around)\n+ *     returns -EINVAL, as does a range whose end (start + PAGE_ALIGN(len_in))\n+ *     would wrap below start.\n+ *\n+ * param: behavior\n+ *   type: int, input\n+ *   cdesc: One of the MADV_* constants from \u003csys/mman.h\u003e. See the long\n+ *     description above for the full list and the three semantic groups\n+ *     (hint, immediate-action, destructive). Behaviors gated by Kconfig\n+ *     (KSM, transparent hugepage, memory failure) return -EINVAL when the\n+ *     underlying support is disabled. A few architectures (notably alpha)\n+ *     renumber values; portable code should always use the symbolic names.\n+ *\n+ * return:\n+ *   type: int\n+ *   check-type: exact\n+ *   success: 0\n+ *   desc: On success, returns 0. On error, returns a negative error code.\n+ *     There is no partial-success indication; either the entire processed\n+ *     range succeeded, or an error is returned and an unspecified prefix of\n+ *     the range may have been advised.\n+ *\n+ * error: EINVAL, Invalid argument\n+ *   desc: Invalid input (unknown or Kconfig-disabled MADV_*, unaligned start,\n+ *     range wrap, non-zero len_in rounding up to zero) or a VMA filter.\n+ *     DONTNEED/FREE reject VM_PFNMAP, VM_LOCKED (not DONTNEED_LOCKED) and\n+ *     misaligned hugetlb; FREE non-anonymous; WIPEONFORK file or shared; REMOVE\n+ *     VM_LOCKED or no file; COLD/PAGEOUT LOCKED/PFNMAP/HUGETLB; DOFORK\n+ *     VM_SPECIAL; KEEPONFORK VM_DROPPABLE; DODUMP SPECIAL/DROPPABLE; GUARD_*\n+ *     SPECIAL/HUGETLB (INSTALL also LOCKED); COLLAPSE if not possible;\n+ *     POPULATE_* on bad permissions.\n+ *\n+ * error: ENOMEM, Cannot allocate memory\n+ *   desc: For VMA-walking behaviors, a gap between mapped VMAs inside the range\n+ *     gives -ENOMEM after the mapped subranges have been processed, unless a\n+ *     per-VMA error ends the walk first. MADV_POPULATE_* stops at the first\n+ *     failure and returns -ENOMEM when the region has no VMA or\n+ *     faultin_page_range() exhausts memory. MADV_COLLAPSE returns -ENOMEM when\n+ *     its struct collapse_control cannot be allocated, and when\n+ *     madvise_collapse_errno() maps SCAN_ALLOC_HUGE_PAGE_FAIL (no hugepage\n+ *     available) to it.\n+ *\n+ * error: EAGAIN, Resource temporarily unavailable\n+ *   desc: For the VMA-flag-mutating behaviors, an internal -ENOMEM from VMA\n+ *     splitting is translated to -EAGAIN before being returned to userspace,\n+ *     advising the caller that a transient kernel resource shortage\n+ *     prevented the update. Also returned by MADV_COLLAPSE via\n+ *     madvise_collapse_errno() for transient scan failures (folio lock\n+ *     contention, LRU isolation failure, dirty/writeback) where retrying\n+ *     the call may succeed.\n+ *\n+ * error: EIO, Input/output error\n+ *   desc: For MADV_REMOVE, an I/O error from the underlying filesystem's\n+ *     FALLOC_FL_PUNCH_HOLE handler is propagated back as -EIO, and\n+ *     MADV_SOFT_OFFLINE returns -EIO when soft_offline_page() cannot handle\n+ *     the page. MADV_HWPOISON reports an unhandled page as -EBUSY; -EIO can\n+ *     reach it only from a ZONE_DEVICE pagemap's -\u003ememory_failure()\n+ *     callback. MADV_WILLNEED and MADV_PAGEOUT do not surface filesystem or\n+ *     device I/O errors: vfs_fadvise() returns are discarded by\n+ *     madvise_willneed() and the pageout walk is invoked through a void\n+ *     helper, so transient I/O failures during read-ahead or page-out are\n+ *     silently dropped.\n+ *\n+ * error: EBADF, Bad file descriptor\n+ *   desc: Returned by MADV_WILLNEED when applied to a non-file-backed VMA\n+ *     and the kernel was built without CONFIG_SWAP, so there is neither a\n+ *     file to read-ahead from nor a swap device to fault from.\n+ *\n+ * error: EACCES, Permission denied\n+ *   desc: Returned by MADV_REMOVE when the target VMA fails\n+ *     vma_is_shared_maywrite(), which needs both VM_SHARED and VM_MAYWRITE.\n+ *     Private file mappings and shared mappings of files not opened for\n+ *     writing are refused, while a PROT_READ MAP_SHARED mapping of a file\n+ *     opened O_RDWR is accepted. Punching a hole through a refused mapping\n+ *     would either be invisible to other mappers or bypass file write\n+ *     permission.\n+ *\n+ * error: EPERM, Operation not permitted\n+ *   desc: Returned in two situations. First, MADV_HWPOISON and\n+ *     MADV_SOFT_OFFLINE require CAP_SYS_ADMIN; the inject-error handler\n+ *     refuses non-privileged callers. Second, on 64-bit kernels, a discard\n+ *     operation (MADV_FREE, MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_REMOVE,\n+ *     MADV_DONTFORK, MADV_WIPEONFORK, MADV_GUARD_INSTALL) is refused on a\n+ *     read-only anonymous VMA that has been sealed with mseal(2), to prevent\n+ *     bypassing the seal by discarding mapped data.\n+ *\n+ * error: EINTR, Interrupted system call\n+ *   desc: Returned when a fatal signal is delivered while the call is\n+ *     waiting to acquire the mmap write lock for a VMA-flag-mutating\n+ *     behavior (mmap_write_lock_killable() returns -EINTR), or when\n+ *     MADV_POPULATE_READ/MADV_POPULATE_WRITE is interrupted while faulting\n+ *     in pages (faultin_page_range() returns -EINTR). Only a fatal signal\n+ *     interrupts these waits, so the task is being killed and user space\n+ *     does not normally see the error.\n+ *\n+ * error: EHWPOISON, Memory page has hardware error\n+ *   desc: MADV_POPULATE_READ or MADV_POPULATE_WRITE encountered a page that\n+ *     has been marked as containing a hardware-detected memory error and\n+ *     could not be faulted in. MADV_HWPOISON also returns it when\n+ *     memory_failure() finds the page already poisoned.\n+ *\n+ * error: EFAULT, Bad address\n+ *   desc: MADV_POPULATE_READ or MADV_POPULATE_WRITE attempted to fault in a\n+ *     page whose mapping raised VM_FAULT_SIGBUS or VM_FAULT_SIGSEGV (for\n+ *     example, a file-backed page beyond the end of the file).\n+ *     MADV_HWPOISON and MADV_SOFT_OFFLINE return a get_user_pages_fast()\n+ *     failure, typically -EFAULT for an unmapped address.\n+ *\n+ * error: EBUSY, Device or resource busy\n+ *   desc: Returned by MADV_COLLAPSE via madvise_collapse_errno() in two\n+ *     specific scan-failure modes: SCAN_CGROUP_CHARGE_FAIL (the new\n+ *     hugepage cannot be charged to the memory cgroup) and\n+ *     SCAN_EXCEED_NONE_PTE (too many absent PTEs in the candidate range\n+ *     for a synchronous collapse). Other transient collapse failures are\n+ *     reported as -EAGAIN; non-transient ones as -EINVAL. MADV_HWPOISON and\n+ *     MADV_SOFT_OFFLINE also return -EBUSY when the page cannot be handled.\n+ *\n+ * error: EOPNOTSUPP, Operation not supported\n+ *   desc: MADV_REMOVE propagates vfs_fallocate() errors verbatim, so a\n+ *     filesystem without FALLOC_FL_PUNCH_HOLE support fails with -EOPNOTSUPP\n+ *     (other propagated errors include -EPERM, -ETXTBSY and -ENOSPC).\n+ *     MADV_SOFT_OFFLINE also returns it when soft offlining is disabled via\n+ *     /proc/sys/vm/enable_soft_offline or the event is filtered by\n+ *     hwpoison_filter(); MADV_HWPOISON folds that case to 0.\n+ *\n+ * lock: mm-\u003emmap_lock (read mode)\n+ *   type: semaphore\n+ *   acquired: yes\n+ *   released: yes\n+ *   desc: Taken for read for MADV_REMOVE, MADV_WILLNEED, MADV_COLD,\n+ *     MADV_PAGEOUT, MADV_COLLAPSE, MADV_POPULATE_READ and\n+ *     MADV_POPULATE_WRITE, and as the fallback when the per-VMA lock path\n+ *     declines. It is dropped and retaken around vfs_fadvise() (WILLNEED on\n+ *     regular files), vfs_fallocate() and userfaultfd_remove() (REMOVE, and\n+ *     DONTNEED or FREE with UFFD_FEATURE_EVENT_REMOVE), the anon and file\n+ *     collapse paths (COLLAPSE) and inside faultin_page_range()\n+ *     (POPULATE_*), so the VMA must be looked up again afterwards.\n+ *\n+ * lock: mm-\u003emmap_lock (write mode; killable)\n+ *   type: semaphore\n+ *   acquired: yes\n+ *   released: yes\n+ *   desc: Acquired in killable write mode for behaviors that modify\n+ *     vma-\u003evm_flags or split/merge VMAs (MADV_NORMAL, MADV_RANDOM,\n+ *     MADV_SEQUENTIAL, MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP,\n+ *     MADV_WIPEONFORK, MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE,\n+ *     MADV_HUGEPAGE, MADV_NOHUGEPAGE). If the acquisition is killed by a\n+ *     fatal signal, the syscall returns -EINTR before any VMA is touched.\n+ *\n+ * lock: per-VMA read lock (vma-\u003evm_refcnt)\n+ *   type: custom\n+ *   acquired: yes\n+ *   released: yes\n+ *   desc: Tried first for MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE,\n+ *     MADV_GUARD_INSTALL and MADV_GUARD_REMOVE via lock_vma_under_rcu(). The\n+ *     lock is a reference on the VMA (vm_refcnt), not an rwsem. The per-VMA\n+ *     path is taken only when the requested range fits within a single VMA,\n+ *     the target mm is the caller's mm, the VMA is not armed with\n+ *     userfaultfd, and, for MADV_GUARD_INSTALL on an anonymous VMA, an\n+ *     anon_vma is already attached. Otherwise the code falls back to the\n+ *     mmap read lock above.\n+ *\n+ * lock: mmu_gather TLB batch\n+ *   type: custom\n+ *   acquired: yes\n+ *   released: yes\n+ *   desc: For MADV_DONTNEED, MADV_DONTNEED_LOCKED and MADV_FREE a single\n+ *     tlb_gather_mmu() / tlb_finish_mmu() pair (madvise_init_tlb() and\n+ *     madvise_finish_tlb()) wraps the whole syscall, batching TLB invalidation\n+ *     across all VMAs in the range. MADV_COLD and MADV_PAGEOUT build a\n+ *     short-lived gather inside the handler. MADV_GUARD_INSTALL builds a\n+ *     transient gather via zap_vma_range() each time the retry loop clears\n+ *     pre-existing pages, and none if the range is already empty.\n+ *     MADV_GUARD_REMOVE never gathers.\n+ *\n+ * lock: mmu_notifier invalidate range\n+ *   type: custom\n+ *   acquired: yes\n+ *   released: yes\n+ *   desc: All zap-based paths -- MADV_DONTNEED, MADV_DONTNEED_LOCKED, the\n+ *     zap branch of MADV_GUARD_INSTALL via zap_vma_range(), and\n+ *     MADV_FREE's own walk -- bracket their work with\n+ *     mmu_notifier_invalidate_range_start()/_end() so secondary MMUs (KVM,\n+ *     IOMMU SVA, etc.) observe the page clearing.\n+ *\n+ * signal: Any fatal signal\n+ *   direction: receive\n+ *   action: return\n+ *   condition: Acquiring the mmap write lock or faulting in pages for\n+ *     MADV_POPULATE_*\n+ *   desc: A pending fatal signal aborts mmap_write_lock_killable() (used by\n+ *     the VMA-flag-mutating behaviors) and faultin_page_range() (used by\n+ *     MADV_POPULATE_READ and MADV_POPULATE_WRITE), in both cases surfacing as\n+ *     -EINTR. Only a fatal signal interrupts these waits, so the task is\n+ *     being killed and user space does not normally see the error.\n+ *   errno: -EINTR\n+ *   timing: during\n+ *   restartable: no\n+ *\n+ * side-effect: modify_state\n+ *   target: vma-\u003evm_flags\n+ *   condition: Hint-group behaviors (MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL,\n+ *     MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP, MADV_WIPEONFORK,\n+ *     MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE, MADV_HUGEPAGE,\n+ *     MADV_NOHUGEPAGE)\n+ *   desc: Sets or clears VM_RAND_READ, VM_SEQ_READ, VM_DONTCOPY,\n+ *     VM_DONTDUMP, VM_WIPEONFORK, VM_MERGEABLE, VM_HUGEPAGE or VM_NOHUGEPAGE\n+ *     on the affected VMAs, splitting or merging VMAs as needed. Reversible\n+ *     with the inverse advice (MADV_DOFORK undoes MADV_DONTFORK), except\n+ *     that the inverse call's VMA filter still applies (DOFORK rejects\n+ *     VM_SPECIAL, DODUMP rejects non-hugetlb VM_SPECIAL or VM_DROPPABLE,\n+ *     KEEPONFORK rejects VM_DROPPABLE).\n+ *   reversible: yes\n+ *\n+ * side-effect: free_memory | modify_state | irreversible\n+ *   target: page tables and resident pages within the range\n+ *   condition: MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE\n+ *   desc: MADV_DONTNEED zaps PTEs, releasing the pages or swap slots so the\n+ *     next access faults in zero-filled anonymous pages or re-reads the\n+ *     file. MADV_DONTNEED_LOCKED is identical but tolerates VM_LOCKED.\n+ *     MADV_FREE marks anonymous pages lazy-freeable; clean pages may be\n+ *     reclaimed under memory pressure and a write before reclaim cancels\n+ *     the lazy free. Discarded data cannot be recovered.\n+ *   reversible: no\n+ *\n+ * side-effect: filesystem | irreversible\n+ *   target: backing file (FALLOC_FL_PUNCH_HOLE)\n+ *   condition: MADV_REMOVE\n+ *   desc: Calls vfs_fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE) on\n+ *     the backing file, deallocating the corresponding file blocks. The hole\n+ *     is visible to all mappers of the file and to read(2)/write(2)\n+ *     callers; subsequent reads return zero. Filesystem freeze protection,\n+ *     i_rwsem and any quota/space accounting are taken by the underlying\n+ *     fallocate path.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state | schedule\n+ *   target: LRU lists and page reclaim\n+ *   condition: MADV_COLD, MADV_PAGEOUT\n+ *   desc: MADV_COLD deactivates the affected pages, moving them to the\n+ *     inactive LRU and clearing PG_referenced/PG_young so they are reclaimed\n+ *     sooner under pressure. MADV_PAGEOUT additionally calls reclaim_pages()\n+ *     to write dirty pages out and drop clean ones synchronously. Page data\n+ *     is preserved (rereads will fault in the same content), but the I/O and\n+ *     LRU bookkeeping cannot be undone.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: page tables (faultin)\n+ *   condition: MADV_POPULATE_READ, MADV_POPULATE_WRITE\n+ *   desc: Walks the requested range with faultin_page_range(), populating\n+ *     PTEs by triggering read or write faults so subsequent accesses do not\n+ *     fault. Equivalent to touching every page in the range while suppressing\n+ *     SIGBUS/SIGSEGV through the syscall return value. Allocations made by\n+ *     faultin are not undone on partial failure.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state | schedule\n+ *   target: transparent hugepage layout\n+ *   condition: MADV_COLLAPSE\n+ *   desc: Synchronously coalesces base pages in the range into a PMD-sized\n+ *     transparent hugepage when the mapping permits. Performs the same page\n+ *     migration and zeroing that khugepaged would do asynchronously; the\n+ *     range's data is preserved across the collapse.\n+ *   reversible: no\n+ *\n+ * side-effect: free_memory | modify_state | irreversible\n+ *   target: PTE marker (PTE_MARKER_GUARD)\n+ *   condition: MADV_GUARD_INSTALL, MADV_GUARD_REMOVE\n+ *   desc: MADV_GUARD_INSTALL installs PTE_MARKER_GUARD entries that cause\n+ *     subsequent accesses to deliver SIGSEGV without consuming physical\n+ *     memory; existing pages already mapped in the range are zapped via\n+ *     zap_vma_range() before the markers are installed, so any\n+ *     prior contents are lost. MADV_GUARD_REMOVE clears the markers but\n+ *     does not (and cannot) restore zapped data.\n+ *   reversible: no\n+ *\n+ * side-effect: hardware | irreversible\n+ *   target: physical page (memory_failure)\n+ *   condition: MADV_HWPOISON\n+ *   desc: MADV_HWPOISON marks the affected pages as containing an\n+ *     unrecoverable hardware error using the same machine-check path that\n+ *     real ECC failures take. This affects physical memory bookkeeping\n+ *     kernel-wide, and madvise() has no inverse. The poison can be cleared\n+ *     through unpoison_memory() (the hwpoison-inject debugfs interface).\n+ *     Intended for testing the memory-failure pipeline; restricted to\n+ *     CAP_SYS_ADMIN.\n+ *   reversible: no\n+ *\n+ * side-effect: hardware\n+ *   target: physical page (soft_offline_page)\n+ *   condition: MADV_SOFT_OFFLINE\n+ *   desc: Migrates the contents off the affected pages and removes them from\n+ *     the buddy allocator. Page contents are preserved. madvise() has no\n+ *     inverse. The pages can be brought back through unpoison_memory() (the\n+ *     hwpoison-inject debugfs interface). Intended for testing the\n+ *     memory-failure pipeline; restricted to CAP_SYS_ADMIN.\n+ *   reversible: no\n+ *\n+ * side-effect: modify_state\n+ *   target: KSM merge state (vm_flags \u0026 VM_MERGEABLE)\n+ *   condition: MADV_MERGEABLE, MADV_UNMERGEABLE\n+ *   desc: Toggles VM_MERGEABLE, the VMA's eligibility for the kernel\n+ *     same-page merger. MADV_MERGEABLE lets ksmd later replace identical\n+ *     anonymous pages with shared, write-protected copies and is silently\n+ *     ignored on KSM-incompatible VMAs. MADV_UNMERGEABLE synchronously\n+ *     calls break_ksm(), which faults each KSM page in the range back to an\n+ *     exclusive copy, before clearing VM_MERGEABLE. An -ENOMEM from either\n+ *     advice is returned as -EAGAIN. The flag toggle is reversible by\n+ *     issuing the inverse advice.\n+ *   reversible: yes\n+ *\n+ * side-effect: modify_state\n+ *   target: userfaultfd event queue\n+ *   condition: MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE, MADV_REMOVE\n+ *     on a VMA whose userfaultfd context negotiated UFFD_FEATURE_EVENT_REMOVE\n+ *   desc: Generates a UFFD_EVENT_REMOVE notification covering the discarded\n+ *     range so userfaultfd monitors observing the mapping see the\n+ *     invalidation. The event is queued before the discard takes effect; the\n+ *     monitor cannot veto it.\n+ *   reversible: no\n+ *\n+ * capability: CAP_SYS_ADMIN\n+ *   type: perform_operation\n+ *   allows: Inject memory errors via MADV_HWPOISON or MADV_SOFT_OFFLINE\n+ *   without: Both behaviors return -EPERM\n+ *   condition: Checked at entry to madvise_inject_error() before any pages\n+ *     are looked up\n+ *\n+ * constraint: Page-aligned start\n+ *   desc: start must lie on a page boundary; otherwise the call returns\n+ *     -EINVAL before any VMA is consulted.\n+ *   expr: (start \u0026 (PAGE_SIZE - 1)) == 0\n+ *\n+ * constraint: Length rounded up to PAGE_SIZE\n+ *   desc: The effective range length is PAGE_ALIGN(len_in). A non-zero len_in\n+ *     that overflows during rounding, or a (start, end) range that wraps,\n+ *     is rejected with -EINVAL.\n+ *   expr: end = start + PAGE_ALIGN(len_in); end \u003e= start\n+ *\n+ * constraint: Behavior must be supported\n+ *   desc: behavior must be one of the MADV_* values listed under the\n+ *     behavior parameter. Behaviors gated by Kconfig (KSM, THP, memory\n+ *     failure) are rejected with -EINVAL when the corresponding option is\n+ *     disabled in the running kernel.\n+ *\n+ * constraint: mseal-protected discards\n+ *   desc: On 64-bit kernels, a discard operation (FREE, DONTNEED,\n+ *     DONTNEED_LOCKED, REMOVE, DONTFORK, WIPEONFORK, GUARD_INSTALL) against\n+ *     a sealed anonymous VMA is rejected unless the mapping is currently\n+ *     writable -- both VM_WRITE in vm_flags and arch_vma_access_permitted()\n+ *     allowing write -- so that mseal(2) cannot be bypassed by instructing\n+ *     the kernel to throw the data away. File-backed sealed VMAs and\n+ *     writable sealed VMAs are not subject to this restriction.\n+ *   expr: !is_discard(behavior) || !vma_is_sealed(vma) ||\n+ *     !vma_is_anonymous(vma) || ((vma-\u003evm_flags \u0026 VM_WRITE) \u0026\u0026\n+ *     arch_vma_access_permitted(vma, true, false, false))\n+ *\n+ * constraint: MADV_FREE requires anonymous mappings\n+ *   desc: MADV_FREE is defined only over anonymous mappings; the handler\n+ *     requires vma_is_anonymous() (no vm_ops) and rejects file-backed VMAs,\n+ *     including shared anonymous (shmem) VMAs, with -EINVAL.\n+ *   expr: vma_is_anonymous(vma)\n+ *\n+ * constraint: MADV_WIPEONFORK requires private anonymous mappings\n+ *   desc: MADV_WIPEONFORK rejects file-backed mappings and shared anonymous\n+ *     mappings; only MAP_PRIVATE anonymous VMAs accept it. Both rejections\n+ *     surface as -EINVAL.\n+ *   expr: !vma-\u003evm_file \u0026\u0026 !(vma-\u003evm_flags \u0026 VM_SHARED)\n+ *\n+ * constraint: MADV_REMOVE requires a shared file mapping that may be written\n+ *   desc: MADV_REMOVE rejects VM_LOCKED VMAs and VMAs without an associated\n+ *     file/mapping/host inode with -EINVAL. It rejects VMAs failing\n+ *     vma_is_shared_maywrite() (VM_SHARED and VM_MAYWRITE) with -EACCES,\n+ *     which covers private file mappings and shared mappings of files not\n+ *     opened for writing, but not a PROT_READ MAP_SHARED mapping of a file\n+ *     opened O_RDWR.\n+ *   expr: !(vma-\u003evm_flags \u0026 VM_LOCKED) \u0026\u0026 vma-\u003evm_file \u0026\u0026\n+ *     vma-\u003evm_file-\u003ef_mapping \u0026\u0026 vma-\u003evm_file-\u003ef_mapping-\u003ehost \u0026\u0026\n+ *     vma_is_shared_maywrite(vma)\n+ *\n+ * constraint: MADV_COLD / MADV_PAGEOUT VMA filter\n+ *   desc: Both behaviors require LRU-managed pages; they reject VMAs that\n+ *     are mlocked, raw-PFN or hugetlb.\n+ *   expr: !(vma-\u003evm_flags \u0026 (VM_LOCKED | VM_PFNMAP | VM_HUGETLB))\n+ *\n+ * examples: madvise(p, len, MADV_SEQUENTIAL);  // set VM_SEQ_READ on the VMA\n+ *   madvise(p, len, MADV_POPULATE_WRITE);  // prefault writable PTEs\n+ *   madvise(p, len, MADV_DONTNEED);        // discard anonymous pages\n+ *   madvise(p, len, MADV_GUARD_INSTALL);   // install SIGSEGV guard pages\n+ *\n+ * notes: Behavior introduction history (mainline): MADV_FREE in 4.5,\n+ *   MADV_WIPEONFORK / MADV_KEEPONFORK in 4.14, MADV_COLD / MADV_PAGEOUT in\n+ *   5.4, MADV_POPULATE_READ / MADV_POPULATE_WRITE in 5.14,\n+ *   MADV_DONTNEED_LOCKED in 5.18, MADV_COLLAPSE in 6.1, MADV_GUARD_INSTALL /\n+ *   MADV_GUARD_REMOVE in 6.13. Code that wants to remain portable to older\n+ *   kernels must handle -EINVAL gracefully and fall back.\n+ *\n+ *   process_madvise(2) applies the same advice values to another process\n+ *   identified by a pidfd. When the target mm is the caller's own (the\n+ *   pidfd refers to the caller), any locally-supported MADV_* value is\n+ *   accepted. When the target is a different mm, the behavior must be in\n+ *   the non-destructive remote subset (MADV_COLD, MADV_PAGEOUT,\n+ *   MADV_WILLNEED, MADV_COLLAPSE) or the call returns -EINVAL, and the\n+ *   caller must hold CAP_SYS_NICE.\n+ *\n+ *   MADV_PAGEOUT on a non-anonymous VM_MAYSHARE mapping is a silent no-op\n+ *   returning 0 unless can_do_file_pageout() holds, meaning the caller owns\n+ *   the file or is capable over it (file_owner_or_capable(), which honors\n+ *   the mount idmap) or may write it (file_permission(MAY_WRITE)). On a\n+ *   private file mapping that fails the same test, only anonymous pages are\n+ *   paged out. MADV_COLD has no such filter.\n+ *\n+ *   MADV_GUARD_INSTALL retries up to MAX_MADVISE_GUARD_RETRIES (3) times\n+ *   when it loses races with concurrent faulting or khugepaged. If those\n+ *   retries are exhausted the handler returns -ERESTARTNOINTR via\n+ *   restart_syscall(), which sets TIF_SIGPENDING so the return path runs\n+ *   signal handling. madvise() is then transparently re-executed with the\n+ *   same arguments, after any handler for a pending signal has run, unless\n+ *   a fatal signal terminates the task first. The caller never observes an\n+ *   errno from the restart itself and the call appears to make eventual\n+ *   forward progress.\n+ *\n+ *   anon_vma_prepare() failures inside MADV_GUARD_INSTALL bypass the\n+ *   ENOMEM-to-EAGAIN translation that applies to the VMA-flag-mutating\n+ *   behaviors and surface as -ENOMEM directly.\n+ *\n+ *   Architecture note: alpha defines MADV_DONTNEED as 6 (not 4) and reserves\n+ *   MADV_SPACEAVAIL=5; portable code must use the symbolic names from\n+ *   \u003csys/mman.h\u003e.\n+ */\n SYSCALL_DEFINE3(madvise, unsigned long, start, size_t, len_in, int, behavior)\n {\n \treturn do_madvise(current-\u003emm, start, len_in, behavior);\ndiff --git a/scripts/Makefile.build b/scripts/Makefile.build\nindex 4349108e75e1f..6fa9ae47fb2c8 100644\n--- a/scripts/Makefile.build\n+++ b/scripts/Makefile.build\n@@ -175,6 +175,32 @@ ifneq ($(KBUILD_EXTRA_WARN),)\n endif\n endif\n \n+ifeq ($(CONFIG_KAPI_SPEC),y)\n+# kernel-doc -apispec emits a spec only for comments with two or more KAPI\n+# sections; pick files with a contexts: (or context-flags:) line and one more\n+# KAPI section.\n+has-apispec = $(if $(strip $(1)),$(shell \\\n+\tgrep -lE '^[[:space:]]*\\*[[:space:]]*(contexts|context-flags):' $(1) 2\u003e/dev/null | \\\n+\txargs -r grep -lE '^[[:space:]]*\\*[[:space:]]*(api-type|param|error|capability|signal|lock|state-trans|constraint|side-effect|long-desc):'))\n+apispec-c-files := $(call has-apispec, \\\n+\t$(patsubst $(obj)/%.o,$(src)/%.c, \\\n+\t\t$(filter-out %/built-in.a,$(real-obj-y))))\n+apispec-y := $(patsubst $(src)/%.c,$(obj)/%.apispec.h,$(apispec-c-files))\n+always-y += $(apispec-y)\n+targets += $(apispec-y)\n+\n+quiet_cmd_apispec = APISPEC $@\n+      cmd_apispec = PYTHONDONTWRITEBYTECODE=1 $(PYTHON3) $(KERNELDOC) -apispec \\\n+                    $(KDOCFLAGS) $\u003c \u003e $@\n+\n+$(obj)/%.apispec.h: $(src)/%.c $(KERNELDOC) \\\n+\t\t$(wildcard $(srctree)/tools/lib/python/kdoc/*.py) FORCE\n+\t$(call if_changed,apispec)\n+\n+$(apispec-y:.apispec.h=.o): $(obj)/%.o: $(obj)/%.apispec.h\n+$(apispec-y:.apispec.h=.o): private c_flags += -include $(obj)/$*.apispec.h\n+endif\n+\n # Compile C sources (.c)\n # ---------------------------------------------------------------------------\n \ndiff --git a/tools/docs/kernel-doc b/tools/docs/kernel-doc\nindex d9192c3f1645e..4c45c0d6cfa94 100755\n--- a/tools/docs/kernel-doc\n+++ b/tools/docs/kernel-doc\n@@ -250,6 +250,8 @@ def main():\n                          help=\"Output reStructuredText format (default).\")\n     out_fmt.add_argument(\"-N\", \"-none\", \"--none\", action=\"store_true\",\n                          help=\"Do not output documentation, only warnings.\")\n+    out_fmt.add_argument(\"-apispec\", \"--apispec\", action=\"store_true\",\n+                         help=\"Output C macro invocations for kernel API specifications.\")\n \n     out_fmt.add_argument(\"-y\", \"--yaml-file\", \"--yaml\",\n                          help=\"Stores kernel-doc output on a yaml file.\")\n@@ -326,6 +328,7 @@ def main():\n     #\n     from kdoc.kdoc_files import KernelFiles             # pylint: disable=C0415\n     from kdoc.kdoc_output import RestFormat, ManFormat  # pylint: disable=C0415\n+    from kdoc.kdoc_apispec import ApiSpecFormat         # pylint: disable=C0415\n \n     yaml_content = set()\n     if args.yaml_file:\n@@ -351,12 +354,16 @@ def main():\n             out_style = None\n             n_outputs += 1\n \n+        if args.apispec:\n+            out_style = ApiSpecFormat()\n+            n_outputs += 1\n+\n         if  args.rst or n_outputs == 0:\n             n_outputs += 1\n             out_style = RestFormat()\n \n         if n_outputs \u003e 1:\n-             parser.error(\"Those arguments are muttually exclusive: --man, --rst, --none, except when generating a YAML file.\")\n+             parser.error(\"Those arguments are muttually exclusive: --man, --rst, --none, --apispec, except when generating a YAML file.\")\n \n         elif not n_outputs:\n             out_style = RestFormat()\n@@ -365,7 +372,8 @@ def main():\n                          yaml_file=args.yaml_file, yaml_content=yaml_content,\n                          out_style=out_style, werror=args.werror,\n                          wreturn=args.wreturn, wshort_desc=args.wshort_desc,\n-                         wcontents_before_sections=args.wcontents_before_sections)\n+                         wcontents_before_sections=args.wcontents_before_sections,\n+                         apispec=args.apispec)\n \n     kfiles.parse(args.files, export_file=args.export_file)\n \ndiff --git a/tools/kapi/.gitignore b/tools/kapi/.gitignore\nnew file mode 100644\nindex 0000000000000..fa95032ed6c2a\n--- /dev/null\n+++ b/tools/kapi/.gitignore\n@@ -0,0 +1,3 @@\n+# Rust build artifacts\n+/target/\n+**/*.rs.bk\ndiff --git a/tools/kapi/Cargo.lock b/tools/kapi/Cargo.lock\nnew file mode 100644\nindex 0000000000000..23d4ef8b910d2\n--- /dev/null\n+++ b/tools/kapi/Cargo.lock\n@@ -0,0 +1,679 @@\n+# This file is automatically @generated by Cargo.\n+# It is not intended for manual editing.\n+version = 4\n+\n+[[package]]\n+name = \"aho-corasick\"\n+version = \"1.1.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301\"\n+dependencies = [\n+ \"memchr\",\n+]\n+\n+[[package]]\n+name = \"anstream\"\n+version = \"1.0.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d\"\n+dependencies = [\n+ \"anstyle\",\n+ \"anstyle-parse\",\n+ \"anstyle-query\",\n+ \"anstyle-wincon\",\n+ \"colorchoice\",\n+ \"is_terminal_polyfill\",\n+ \"utf8parse\",\n+]\n+\n+[[package]]\n+name = \"anstyle\"\n+version = \"1.0.14\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000\"\n+\n+[[package]]\n+name = \"anstyle-parse\"\n+version = \"1.0.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e\"\n+dependencies = [\n+ \"utf8parse\",\n+]\n+\n+[[package]]\n+name = \"anstyle-query\"\n+version = \"1.1.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc\"\n+dependencies = [\n+ \"windows-sys\",\n+]\n+\n+[[package]]\n+name = \"anstyle-wincon\"\n+version = \"3.0.11\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d\"\n+dependencies = [\n+ \"anstyle\",\n+ \"once_cell_polyfill\",\n+ \"windows-sys\",\n+]\n+\n+[[package]]\n+name = \"anyhow\"\n+version = \"1.0.102\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c\"\n+\n+[[package]]\n+name = \"bitflags\"\n+version = \"2.11.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3\"\n+\n+[[package]]\n+name = \"cfg-if\"\n+version = \"1.0.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801\"\n+\n+[[package]]\n+name = \"clap\"\n+version = \"4.6.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51\"\n+dependencies = [\n+ \"clap_builder\",\n+ \"clap_derive\",\n+]\n+\n+[[package]]\n+name = \"clap_builder\"\n+version = \"4.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f\"\n+dependencies = [\n+ \"anstream\",\n+ \"anstyle\",\n+ \"clap_lex\",\n+ \"strsim\",\n+]\n+\n+[[package]]\n+name = \"clap_derive\"\n+version = \"4.6.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9\"\n+dependencies = [\n+ \"heck\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"clap_lex\"\n+version = \"1.1.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9\"\n+\n+[[package]]\n+name = \"colorchoice\"\n+version = \"1.0.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570\"\n+\n+[[package]]\n+name = \"equivalent\"\n+version = \"1.0.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f\"\n+\n+[[package]]\n+name = \"errno\"\n+version = \"0.3.14\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb\"\n+dependencies = [\n+ \"libc\",\n+ \"windows-sys\",\n+]\n+\n+[[package]]\n+name = \"fastrand\"\n+version = \"2.4.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6\"\n+\n+[[package]]\n+name = \"foldhash\"\n+version = \"0.1.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2\"\n+\n+[[package]]\n+name = \"getrandom\"\n+version = \"0.4.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"libc\",\n+ \"r-efi\",\n+ \"wasip2\",\n+ \"wasip3\",\n+]\n+\n+[[package]]\n+name = \"goblin\"\n+version = \"0.10.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9\"\n+dependencies = [\n+ \"log\",\n+ \"plain\",\n+ \"scroll\",\n+]\n+\n+[[package]]\n+name = \"hashbrown\"\n+version = \"0.15.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1\"\n+dependencies = [\n+ \"foldhash\",\n+]\n+\n+[[package]]\n+name = \"hashbrown\"\n+version = \"0.17.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51\"\n+\n+[[package]]\n+name = \"heck\"\n+version = \"0.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea\"\n+\n+[[package]]\n+name = \"id-arena\"\n+version = \"2.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954\"\n+\n+[[package]]\n+name = \"indexmap\"\n+version = \"2.14.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9\"\n+dependencies = [\n+ \"equivalent\",\n+ \"hashbrown 0.17.0\",\n+ \"serde\",\n+ \"serde_core\",\n+]\n+\n+[[package]]\n+name = \"is_terminal_polyfill\"\n+version = \"1.70.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695\"\n+\n+[[package]]\n+name = \"itoa\"\n+version = \"1.0.18\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682\"\n+\n+[[package]]\n+name = \"kapi\"\n+version = \"0.1.0\"\n+dependencies = [\n+ \"anyhow\",\n+ \"clap\",\n+ \"goblin\",\n+ \"regex\",\n+ \"serde\",\n+ \"serde_json\",\n+ \"tempfile\",\n+ \"walkdir\",\n+]\n+\n+[[package]]\n+name = \"leb128fmt\"\n+version = \"0.1.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2\"\n+\n+[[package]]\n+name = \"libc\"\n+version = \"0.2.185\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f\"\n+\n+[[package]]\n+name = \"linux-raw-sys\"\n+version = \"0.12.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53\"\n+\n+[[package]]\n+name = \"log\"\n+version = \"0.4.29\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897\"\n+\n+[[package]]\n+name = \"memchr\"\n+version = \"2.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79\"\n+\n+[[package]]\n+name = \"once_cell\"\n+version = \"1.21.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50\"\n+\n+[[package]]\n+name = \"once_cell_polyfill\"\n+version = \"1.70.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe\"\n+\n+[[package]]\n+name = \"plain\"\n+version = \"0.2.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6\"\n+\n+[[package]]\n+name = \"prettyplease\"\n+version = \"0.2.37\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"proc-macro2\"\n+version = \"1.0.106\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934\"\n+dependencies = [\n+ \"unicode-ident\",\n+]\n+\n+[[package]]\n+name = \"quote\"\n+version = \"1.0.45\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924\"\n+dependencies = [\n+ \"proc-macro2\",\n+]\n+\n+[[package]]\n+name = \"r-efi\"\n+version = \"6.0.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf\"\n+\n+[[package]]\n+name = \"regex\"\n+version = \"1.12.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276\"\n+dependencies = [\n+ \"aho-corasick\",\n+ \"memchr\",\n+ \"regex-automata\",\n+ \"regex-syntax\",\n+]\n+\n+[[package]]\n+name = \"regex-automata\"\n+version = \"0.4.14\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f\"\n+dependencies = [\n+ \"aho-corasick\",\n+ \"memchr\",\n+ \"regex-syntax\",\n+]\n+\n+[[package]]\n+name = \"regex-syntax\"\n+version = \"0.8.10\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a\"\n+\n+[[package]]\n+name = \"rustix\"\n+version = \"1.1.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190\"\n+dependencies = [\n+ \"bitflags\",\n+ \"errno\",\n+ \"libc\",\n+ \"linux-raw-sys\",\n+ \"windows-sys\",\n+]\n+\n+[[package]]\n+name = \"same-file\"\n+version = \"1.0.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502\"\n+dependencies = [\n+ \"winapi-util\",\n+]\n+\n+[[package]]\n+name = \"scroll\"\n+version = \"0.13.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add\"\n+dependencies = [\n+ \"scroll_derive\",\n+]\n+\n+[[package]]\n+name = \"scroll_derive\"\n+version = \"0.13.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"semver\"\n+version = \"1.0.28\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd\"\n+\n+[[package]]\n+name = \"serde\"\n+version = \"1.0.228\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e\"\n+dependencies = [\n+ \"serde_core\",\n+ \"serde_derive\",\n+]\n+\n+[[package]]\n+name = \"serde_core\"\n+version = \"1.0.228\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad\"\n+dependencies = [\n+ \"serde_derive\",\n+]\n+\n+[[package]]\n+name = \"serde_derive\"\n+version = \"1.0.228\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"serde_json\"\n+version = \"1.0.149\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86\"\n+dependencies = [\n+ \"itoa\",\n+ \"memchr\",\n+ \"serde\",\n+ \"serde_core\",\n+ \"zmij\",\n+]\n+\n+[[package]]\n+name = \"strsim\"\n+version = \"0.11.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f\"\n+\n+[[package]]\n+name = \"syn\"\n+version = \"2.0.117\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"unicode-ident\",\n+]\n+\n+[[package]]\n+name = \"tempfile\"\n+version = \"3.27.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd\"\n+dependencies = [\n+ \"fastrand\",\n+ \"getrandom\",\n+ \"once_cell\",\n+ \"rustix\",\n+ \"windows-sys\",\n+]\n+\n+[[package]]\n+name = \"unicode-ident\"\n+version = \"1.0.24\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75\"\n+\n+[[package]]\n+name = \"unicode-xid\"\n+version = \"0.2.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853\"\n+\n+[[package]]\n+name = \"utf8parse\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821\"\n+\n+[[package]]\n+name = \"walkdir\"\n+version = \"2.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b\"\n+dependencies = [\n+ \"same-file\",\n+ \"winapi-util\",\n+]\n+\n+[[package]]\n+name = \"wasip2\"\n+version = \"1.0.3+wasi-0.2.9\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6\"\n+dependencies = [\n+ \"wit-bindgen 0.57.1\",\n+]\n+\n+[[package]]\n+name = \"wasip3\"\n+version = \"0.4.0+wasi-0.3.0-rc-2026-01-06\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5\"\n+dependencies = [\n+ \"wit-bindgen 0.51.0\",\n+]\n+\n+[[package]]\n+name = \"wasm-encoder\"\n+version = \"0.244.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319\"\n+dependencies = [\n+ \"leb128fmt\",\n+ \"wasmparser\",\n+]\n+\n+[[package]]\n+name = \"wasm-metadata\"\n+version = \"0.244.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909\"\n+dependencies = [\n+ \"anyhow\",\n+ \"indexmap\",\n+ \"wasm-encoder\",\n+ \"wasmparser\",\n+]\n+\n+[[package]]\n+name = \"wasmparser\"\n+version = \"0.244.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe\"\n+dependencies = [\n+ \"bitflags\",\n+ \"hashbrown 0.15.5\",\n+ \"indexmap\",\n+ \"semver\",\n+]\n+\n+[[package]]\n+name = \"winapi-util\"\n+version = \"0.1.11\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22\"\n+dependencies = [\n+ \"windows-sys\",\n+]\n+\n+[[package]]\n+name = \"windows-link\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5\"\n+\n+[[package]]\n+name = \"windows-sys\"\n+version = \"0.61.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc\"\n+dependencies = [\n+ \"windows-link\",\n+]\n+\n+[[package]]\n+name = \"wit-bindgen\"\n+version = \"0.51.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5\"\n+dependencies = [\n+ \"wit-bindgen-rust-macro\",\n+]\n+\n+[[package]]\n+name = \"wit-bindgen\"\n+version = \"0.57.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e\"\n+\n+[[package]]\n+name = \"wit-bindgen-core\"\n+version = \"0.51.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc\"\n+dependencies = [\n+ \"anyhow\",\n+ \"heck\",\n+ \"wit-parser\",\n+]\n+\n+[[package]]\n+name = \"wit-bindgen-rust\"\n+version = \"0.51.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21\"\n+dependencies = [\n+ \"anyhow\",\n+ \"heck\",\n+ \"indexmap\",\n+ \"prettyplease\",\n+ \"syn\",\n+ \"wasm-metadata\",\n+ \"wit-bindgen-core\",\n+ \"wit-component\",\n+]\n+\n+[[package]]\n+name = \"wit-bindgen-rust-macro\"\n+version = \"0.51.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a\"\n+dependencies = [\n+ \"anyhow\",\n+ \"prettyplease\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+ \"wit-bindgen-core\",\n+ \"wit-bindgen-rust\",\n+]\n+\n+[[package]]\n+name = \"wit-component\"\n+version = \"0.244.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2\"\n+dependencies = [\n+ \"anyhow\",\n+ \"bitflags\",\n+ \"indexmap\",\n+ \"log\",\n+ \"serde\",\n+ \"serde_derive\",\n+ \"serde_json\",\n+ \"wasm-encoder\",\n+ \"wasm-metadata\",\n+ \"wasmparser\",\n+ \"wit-parser\",\n+]\n+\n+[[package]]\n+name = \"wit-parser\"\n+version = \"0.244.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736\"\n+dependencies = [\n+ \"anyhow\",\n+ \"id-arena\",\n+ \"indexmap\",\n+ \"log\",\n+ \"semver\",\n+ \"serde\",\n+ \"serde_derive\",\n+ \"serde_json\",\n+ \"unicode-xid\",\n+ \"wasmparser\",\n+]\n+\n+[[package]]\n+name = \"zmij\"\n+version = \"1.0.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa\"\ndiff --git a/tools/kapi/Cargo.toml b/tools/kapi/Cargo.toml\nnew file mode 100644\nindex 0000000000000..159e65d324468\n--- /dev/null\n+++ b/tools/kapi/Cargo.toml\n@@ -0,0 +1,20 @@\n+[package]\n+name = \"kapi\"\n+version = \"0.1.0\"\n+edition = \"2021\"\n+rust-version = \"1.85\"\n+authors = [\"Sasha Levin \u003csashal@kernel.org\u003e\"]\n+description = \"Tool for extracting and displaying kernel API specifications\"\n+license = \"GPL-2.0\"\n+\n+[dependencies]\n+goblin = \"0.10\"\n+clap = { version = \"4.4\", features = [\"derive\"] }\n+anyhow = \"1.0\"\n+serde = { version = \"1.0\", features = [\"derive\"] }\n+serde_json = \"1.0\"\n+regex = \"1.10\"\n+walkdir = \"2.4\"\n+\n+[dev-dependencies]\n+tempfile = \"3.8\"\ndiff --git a/tools/kapi/Makefile b/tools/kapi/Makefile\nnew file mode 100644\nindex 0000000000000..d4234538e4eee\n--- /dev/null\n+++ b/tools/kapi/Makefile\n@@ -0,0 +1,33 @@\n+# SPDX-License-Identifier: GPL-2.0\n+# Makefile wrapper for the kapi tool (Rust userspace binary).\n+#\n+# See Documentation/dev-tools/kernel-api-spec.rst for details.\n+\n+PREFIX ?= /usr/local\n+\n+.PHONY: all build release debug clean install test fmt clippy\n+\n+all: release\n+\n+release:\n+\tcargo build --release\n+\n+build: release\n+\n+debug:\n+\tcargo build\n+\n+test:\n+\tcargo test\n+\n+fmt:\n+\tcargo fmt --all -- --check\n+\n+clippy:\n+\tcargo clippy --all-targets --all-features -- -D warnings\n+\n+clean:\n+\tcargo clean\n+\n+install: release\n+\tinstall -D -m 0755 target/release/kapi $(DESTDIR)$(PREFIX)/bin/kapi\ndiff --git a/tools/kapi/README.md b/tools/kapi/README.md\nnew file mode 100644\nindex 0000000000000..3839f3ff2964a\n--- /dev/null\n+++ b/tools/kapi/README.md\n@@ -0,0 +1,33 @@\n+# kapi — Kernel API Specification Extractor\n+\n+Userspace utility that extracts and displays kernel API specifications from\n+three sources:\n+\n+- `--source PATH` — parse kerneldoc blocks in a C source file or tree\n+- `--vmlinux PATH` — decode the `.kapi_specs` ELF section of a compiled vmlinux\n+- `--debugfs PATH` — read the live specs from `/sys/kernel/debug/kapi/` on a\n+  running kernel (this is also the mode used when no input option is given, in\n+  which case PATH is `/sys/kernel/debug`)\n+\n+Output formats: `plain` (default), `json`, `rst`.\n+\n+See `Documentation/dev-tools/kernel-api-spec.rst` for the full user guide,\n+including the kerneldoc DSL reference and the surrounding framework design.\n+\n+## Build\n+\n+```\n+make -C tools/kapi\n+```\n+\n+(wraps `cargo build --release`; the binary is produced at\n+`tools/kapi/target/release/kapi`).\n+\n+## Usage\n+\n+```\n+tools/kapi/target/release/kapi --help\n+tools/kapi/target/release/kapi --source fs/open.c sys_open\n+tools/kapi/target/release/kapi --vmlinux vmlinux -f json\n+tools/kapi/target/release/kapi --debugfs /sys/kernel/debug\n+```\ndiff --git a/tools/kapi/src/extractor/debugfs.rs b/tools/kapi/src/extractor/debugfs.rs\nnew file mode 100644\nindex 0000000000000..0a7e41e1c40ca\n--- /dev/null\n+++ b/tools/kapi/src/extractor/debugfs.rs\n@@ -0,0 +1,1704 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use crate::formatter::OutputFormatter;\n+use anyhow::{bail, Context, Result};\n+use serde::Deserialize;\n+use std::fs;\n+use std::io::Write;\n+use std::path::PathBuf;\n+\n+use super::{\n+    display_api_spec, ApiExtractor, ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec,\n+    ParamSpec, ReturnSpec, SignalMaskSpec, StateTransitionSpec, StructFieldSpec, StructSpec,\n+};\n+\n+// Schema matching what kapi_export_json() in the kernel emits.  The kernel\n+// serialises several enum-like fields as hex strings (\"0x%x\") or token\n+// strings (\"exact\", \"process\"); we keep them as Option\u003cString\u003e here and\n+// interpret them during conversion.\n+#[derive(Deserialize)]\n+struct KernelApiJson {\n+    name: String,\n+    #[serde(default)]\n+    api_type: Option\u003cString\u003e,\n+    #[serde(default)]\n+    version: Option\u003cu32\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    long_description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    context_flags: Option\u003cString\u003e,\n+    #[serde(default)]\n+    examples: Option\u003cString\u003e,\n+    #[serde(default)]\n+    notes: Option\u003cString\u003e,\n+    #[serde(default)]\n+    capabilities: Option\u003cVec\u003cKernelCapabilityJson\u003e\u003e,\n+    #[serde(default)]\n+    parameters: Option\u003cVec\u003cKernelParamJson\u003e\u003e,\n+    #[serde(default)]\n+    errors: Option\u003cVec\u003cKernelErrorJson\u003e\u003e,\n+    #[serde(default, rename = \"return\")]\n+    return_spec: Option\u003cKernelReturnJson\u003e,\n+    #[serde(default)]\n+    locks: Option\u003cVec\u003cKernelLockJson\u003e\u003e,\n+    #[serde(default)]\n+    constraints: Option\u003cVec\u003cKernelConstraintJson\u003e\u003e,\n+    #[serde(default)]\n+    signals: Option\u003cVec\u003cKernelSignalJson\u003e\u003e,\n+    #[serde(default)]\n+    side_effects: Option\u003cVec\u003cKernelSideEffectJson\u003e\u003e,\n+    #[serde(default)]\n+    state_transitions: Option\u003cVec\u003cKernelStateTransitionJson\u003e\u003e,\n+    #[serde(default)]\n+    signal_masks: Option\u003cVec\u003cKernelSignalMaskJson\u003e\u003e,\n+    #[serde(default)]\n+    struct_specs: Option\u003cVec\u003cKernelStructJson\u003e\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelStateTransitionJson {\n+    #[serde(default)]\n+    object: Option\u003cString\u003e,\n+    #[serde(default)]\n+    from_state: Option\u003cString\u003e,\n+    #[serde(default)]\n+    to_state: Option\u003cString\u003e,\n+    #[serde(default)]\n+    condition: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelSignalMaskJson {\n+    #[serde(default)]\n+    name: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    signals: Vec\u003ci32\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelStructFieldJson {\n+    #[serde(default)]\n+    name: Option\u003cString\u003e,\n+    #[serde(rename = \"type\", default)]\n+    type_name: Option\u003cString\u003e,\n+    #[serde(default)]\n+    type_class: Option\u003cString\u003e,\n+    #[serde(default)]\n+    offset: usize,\n+    #[serde(default)]\n+    size: usize,\n+    #[serde(default)]\n+    flags: Option\u003cString\u003e,\n+    #[serde(default)]\n+    constraint_type: Option\u003cString\u003e,\n+    #[serde(default)]\n+    min_value: i64,\n+    #[serde(default)]\n+    max_value: i64,\n+    #[serde(default)]\n+    valid_mask: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelStructJson {\n+    #[serde(default)]\n+    name: Option\u003cString\u003e,\n+    #[serde(default)]\n+    size: usize,\n+    #[serde(default)]\n+    alignment: usize,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    fields: Vec\u003cKernelStructFieldJson\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelConstraintJson {\n+    name: String,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    expression: Option\u003cString\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelSignalJson {\n+    #[serde(default)]\n+    signal_num: i32,\n+    #[serde(default)]\n+    signal_name: Option\u003cString\u003e,\n+    #[serde(default)]\n+    direction: Option\u003cString\u003e,\n+    #[serde(default)]\n+    action: u32,\n+    #[serde(default)]\n+    target: Option\u003cString\u003e,\n+    #[serde(default)]\n+    condition: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    restartable: bool,\n+    #[serde(default)]\n+    sa_flags_required: Option\u003cString\u003e,\n+    #[serde(default)]\n+    sa_flags_forbidden: Option\u003cString\u003e,\n+    #[serde(default)]\n+    error_on_signal: i32,\n+    #[serde(default)]\n+    transform_to: i32,\n+    #[serde(default)]\n+    timing: Option\u003cString\u003e,\n+    #[serde(default)]\n+    priority: u32,\n+    #[serde(default)]\n+    interruptible: bool,\n+    #[serde(default)]\n+    queue_behavior: Option\u003cString\u003e,\n+    #[serde(default)]\n+    state_required: Option\u003cString\u003e,\n+    #[serde(default)]\n+    state_forbidden: Option\u003cString\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelSideEffectJson {\n+    #[serde(rename = \"type\", default)]\n+    type_hex: Option\u003cString\u003e,\n+    #[serde(default)]\n+    target: Option\u003cString\u003e,\n+    #[serde(default)]\n+    condition: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    reversible: bool,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelParamJson {\n+    name: String,\n+    #[serde(rename = \"type\", default)]\n+    type_name: Option\u003cString\u003e,\n+    #[serde(default)]\n+    type_class: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    flags: Option\u003cString\u003e,\n+    #[serde(default)]\n+    constraint_type: Option\u003cString\u003e,\n+    #[serde(default)]\n+    constraint_desc: Option\u003cString\u003e,\n+    #[serde(default)]\n+    min_value: Option\u003ci64\u003e,\n+    #[serde(default)]\n+    max_value: Option\u003ci64\u003e,\n+    #[serde(default)]\n+    valid_mask: Option\u003cString\u003e,\n+    #[serde(default)]\n+    enum_values: Vec\u003ci64\u003e,\n+    #[serde(default)]\n+    size: Option\u003cu64\u003e,\n+    #[serde(default)]\n+    alignment: Option\u003cu64\u003e,\n+    #[serde(default)]\n+    size_param_idx: Option\u003cu32\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelErrorJson {\n+    #[serde(rename = \"code\")]\n+    error_code: i32,\n+    #[serde(default)]\n+    name: Option\u003cString\u003e,\n+    #[serde(default)]\n+    condition: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelReturnJson {\n+    #[serde(rename = \"type\", default)]\n+    type_name: Option\u003cString\u003e,\n+    #[serde(default)]\n+    type_class: Option\u003cString\u003e,\n+    #[serde(default)]\n+    check_type: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+    #[serde(default)]\n+    success_value: Option\u003ci64\u003e,\n+    #[serde(default)]\n+    success_min: Option\u003ci64\u003e,\n+    #[serde(default)]\n+    success_max: Option\u003ci64\u003e,\n+    #[serde(default)]\n+    error_values: Vec\u003ci64\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelLockJson {\n+    name: String,\n+    #[serde(rename = \"type\", default)]\n+    lock_type: Option\u003cString\u003e,\n+    #[serde(default)]\n+    scope: Option\u003cString\u003e,\n+    #[serde(default)]\n+    description: Option\u003cString\u003e,\n+}\n+\n+#[derive(Deserialize)]\n+struct KernelCapabilityJson {\n+    capability: i32,\n+    name: String,\n+    action: String,\n+    allows: String,\n+    without_cap: String,\n+    check_condition: Option\u003cString\u003e,\n+    priority: Option\u003cu8\u003e,\n+    alternatives: Option\u003cVec\u003ci32\u003e\u003e,\n+}\n+\n+/// Free-text fields of the text dump that may span several lines.\n+#[derive(Clone, Copy)]\n+enum TextField {\n+    Description,\n+    LongDescription,\n+    Examples,\n+    Notes,\n+}\n+\n+impl TextField {\n+    /// Recognise the header line of a text field, returning the field and\n+    /// whatever follows the colon on the same line.\n+    fn start(line: \u0026str) -\u003e Option\u003c(Self, \u0026str)\u003e {\n+        [\n+            (\"Description:\", Self::Description),\n+            (\"Long description:\", Self::LongDescription),\n+            (\"Examples:\", Self::Examples),\n+            (\"Notes:\", Self::Notes),\n+        ]\n+        .into_iter()\n+        .find_map(|(header, field)| {\n+            line.strip_prefix(header)\n+                .map(|rest| (field, rest.trim_start()))\n+        })\n+    }\n+\n+    fn store(self, spec: \u0026mut ApiSpec, lines: \u0026[String]) {\n+        let text = lines.join(\"\\n\");\n+        let text = text.trim_matches('\\n').trim_end().to_string();\n+        let slot = match self {\n+            Self::Description =\u003e \u0026mut spec.description,\n+            Self::LongDescription =\u003e \u0026mut spec.long_description,\n+            Self::Examples =\u003e \u0026mut spec.examples,\n+            Self::Notes =\u003e \u0026mut spec.notes,\n+        };\n+        *slot = Some(text);\n+    }\n+}\n+\n+/// Extractor for kernel API specifications from debugfs\n+pub struct DebugfsExtractor {\n+    debugfs_path: PathBuf,\n+}\n+\n+impl DebugfsExtractor {\n+    /// Create a new debugfs extractor with the specified debugfs path\n+    pub fn new(debugfs_path: Option\u003cString\u003e) -\u003e Result\u003cSelf\u003e {\n+        let path = match debugfs_path {\n+            Some(p) =\u003e PathBuf::from(p),\n+            None =\u003e PathBuf::from(\"/sys/kernel/debug\"),\n+        };\n+\n+        // Check if the debugfs path exists\n+        if !path.exists() {\n+            bail!(\"Debugfs path does not exist: {}\", path.display());\n+        }\n+\n+        // Check if kapi directory exists\n+        let kapi_path = path.join(\"kapi\");\n+        if !kapi_path.exists() {\n+            bail!(\n+                \"Kernel API debugfs interface not found at: {}\",\n+                kapi_path.display()\n+            );\n+        }\n+\n+        Ok(Self { debugfs_path: path })\n+    }\n+\n+    /// Parse the list file to get all available API names\n+    fn parse_list_file(\u0026self) -\u003e Result\u003cVec\u003cString\u003e\u003e {\n+        let list_path = self.debugfs_path.join(\"kapi/list\");\n+        let content = fs::read_to_string(\u0026list_path)\n+            .with_context(|| format!(\"Failed to read {}\", list_path.display()))?;\n+\n+        let mut apis = Vec::new();\n+        let mut in_list = false;\n+\n+        for line in content.lines() {\n+            if line.contains(\"===\") {\n+                in_list = true;\n+                continue;\n+            }\n+\n+            if in_list \u0026\u0026 line.starts_with(\"Total:\") {\n+                break;\n+            }\n+\n+            if in_list \u0026\u0026 !line.trim().is_empty() {\n+                // Extract API name from lines like \"sys_read - Read from a file descriptor\"\n+                if let Some(name) = line.split(\" - \").next() {\n+                    apis.push(name.trim().to_string());\n+                }\n+            }\n+        }\n+\n+        Ok(apis)\n+    }\n+\n+    /// Convert context flags (emitted by the kernel as a hex string like\n+    /// \"0x21\") into the token list consumed by the formatter.\n+    fn parse_context_flags(flags: \u0026str) -\u003e Vec\u003cString\u003e {\n+        let mut result = Vec::new();\n+        let bits = flags\n+            .strip_prefix(\"0x\")\n+            .or_else(|| flags.strip_prefix(\"0X\"))\n+            .unwrap_or(flags);\n+        let Ok(flags) = u32::from_str_radix(bits, 16) else {\n+            return result;\n+        };\n+\n+        // These values should match KAPI_CTX_* flags from kernel\n+        if flags \u0026 (1 \u003c\u003c 0) != 0 {\n+            result.push(\"KAPI_CTX_PROCESS\".to_string());\n+        }\n+        if flags \u0026 (1 \u003c\u003c 1) != 0 {\n+            result.push(\"KAPI_CTX_SOFTIRQ\".to_string());\n+        }\n+        if flags \u0026 (1 \u003c\u003c 2) != 0 {\n+            result.push(\"KAPI_CTX_HARDIRQ\".to_string());\n+        }\n+        if flags \u0026 (1 \u003c\u003c 3) != 0 {\n+            result.push(\"KAPI_CTX_NMI\".to_string());\n+        }\n+        if flags \u0026 (1 \u003c\u003c 4) != 0 {\n+            result.push(\"KAPI_CTX_ATOMIC\".to_string());\n+        }\n+        if flags \u0026 (1 \u003c\u003c 5) != 0 {\n+            result.push(\"KAPI_CTX_SLEEPABLE\".to_string());\n+        }\n+        if flags \u0026 (1 \u003c\u003c 6) != 0 {\n+            result.push(\"KAPI_CTX_PREEMPT_DISABLED\".to_string());\n+        }\n+        if flags \u0026 (1 \u003c\u003c 7) != 0 {\n+            result.push(\"KAPI_CTX_IRQ_DISABLED\".to_string());\n+        }\n+\n+        result\n+    }\n+\n+    /// Parse a hex-string like \"0x123\" into u64, returning 0 on failure.\n+    fn parse_hex_u64(value: \u0026str) -\u003e u64 {\n+        let bits = value\n+            .strip_prefix(\"0x\")\n+            .or_else(|| value.strip_prefix(\"0X\"))\n+            .unwrap_or(value);\n+        u64::from_str_radix(bits, 16).unwrap_or(0)\n+    }\n+\n+    /// Parse a hex-string like \"0x123\" into u32, returning 0 on failure.\n+    fn parse_hex_u32(value: \u0026str) -\u003e u32 {\n+        u32::try_from(Self::parse_hex_u64(value)).unwrap_or(0)\n+    }\n+\n+    /// Map the type-class token emitted by the kernel (param_type_to_string)\n+    /// back to the numeric enum kapi_param_type.\n+    fn parse_type_class(token: \u0026str) -\u003e u32 {\n+        match token {\n+            \"void\" =\u003e 0,\n+            \"int\" =\u003e 1,\n+            \"uint\" =\u003e 2,\n+            \"pointer\" =\u003e 3,\n+            \"struct\" =\u003e 4,\n+            \"union\" =\u003e 5,\n+            \"enum\" =\u003e 6,\n+            \"function_pointer\" =\u003e 7,\n+            \"array\" =\u003e 8,\n+            \"file_descriptor\" =\u003e 9,\n+            \"user_pointer\" =\u003e 10,\n+            \"pathname\" =\u003e 11,\n+            \"custom\" =\u003e 12,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Map the constraint-type token emitted by the kernel\n+    /// (constraint_type_to_string) back to the numeric enum\n+    /// kapi_constraint_type.\n+    fn parse_constraint_type(token: \u0026str) -\u003e u32 {\n+        match token {\n+            \"none\" =\u003e 0,\n+            \"range\" =\u003e 1,\n+            \"mask\" =\u003e 2,\n+            \"enum\" =\u003e 3,\n+            \"alignment\" =\u003e 4,\n+            \"power_of_two\" =\u003e 5,\n+            \"page_aligned\" =\u003e 6,\n+            \"nonzero\" =\u003e 7,\n+            \"user_string\" =\u003e 8,\n+            \"user_path\" =\u003e 9,\n+            \"user_ptr\" =\u003e 10,\n+            \"buffer\" =\u003e 11,\n+            \"custom\" =\u003e 12,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Derive the API type from the symbol name, the same way the vmlinux\n+    /// extractor does.\n+    fn api_type_from_name(name: \u0026str) -\u003e \u0026'static str {\n+        if name.starts_with(\"sys_\") {\n+            \"syscall\"\n+        } else if name.ends_with(\"_ioctl\") {\n+            \"ioctl\"\n+        } else if name.contains(\"sysfs\") {\n+            \"sysfs\"\n+        } else {\n+            \"function\"\n+        }\n+    }\n+\n+    /// Map the check-type token emitted by the kernel\n+    /// (return_check_type_to_string) back to the u32 enum the formatter wants.\n+    fn parse_check_type(token: \u0026str) -\u003e u32 {\n+        match token {\n+            \"exact\" =\u003e 0,\n+            \"range\" =\u003e 1,\n+            \"error_check\" =\u003e 2,\n+            \"file_descriptor\" =\u003e 3,\n+            \"custom\" =\u003e 4,\n+            \"no_return\" =\u003e 5,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Map the lock-type token emitted by the kernel (lock_type_to_string).\n+    fn parse_lock_type(token: \u0026str) -\u003e u32 {\n+        match token {\n+            \"none\" =\u003e 0,\n+            \"mutex\" =\u003e 1,\n+            \"spinlock\" =\u003e 2,\n+            \"rwlock\" =\u003e 3,\n+            \"seqlock\" =\u003e 4,\n+            \"rcu\" =\u003e 5,\n+            \"semaphore\" =\u003e 6,\n+            \"custom\" =\u003e 7,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Map the lock-scope token emitted by the kernel (lock_scope_to_string).\n+    fn parse_lock_scope(token: \u0026str) -\u003e u32 {\n+        match token {\n+            \"internal\" =\u003e 0,\n+            \"acquires\" =\u003e 1,\n+            \"releases\" =\u003e 2,\n+            \"caller_held\" =\u003e 3,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Map the signal-timing token (e.g. \"during\") back to the u32 enum that\n+    /// the source-side parser produces. Mirrors\n+    /// KerneldocParser::parse_signal_timing in kerneldoc_parser.rs so the\n+    /// debugfs path and the source path agree.\n+    fn parse_signal_timing(token: \u0026str) -\u003e u32 {\n+        match token.trim().to_ascii_lowercase().as_str() {\n+            \"before\" =\u003e 0,\n+            \"during\" =\u003e 1,\n+            \"after\" =\u003e 2,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Convert the capability action token emitted by the kernel\n+    /// (capability_action_to_string) to the KAPI_CAP_* spelling used by the\n+    /// other extractors.\n+    fn parse_capability_action(action: \u0026str) -\u003e String {\n+        match action {\n+            \"bypass_check\" =\u003e \"KAPI_CAP_BYPASS_CHECK\".to_string(),\n+            \"increase_limit\" =\u003e \"KAPI_CAP_INCREASE_LIMIT\".to_string(),\n+            \"override_restriction\" =\u003e \"KAPI_CAP_OVERRIDE_RESTRICTION\".to_string(),\n+            \"grant_permission\" =\u003e \"KAPI_CAP_GRANT_PERMISSION\".to_string(),\n+            \"modify_behavior\" =\u003e \"KAPI_CAP_MODIFY_BEHAVIOR\".to_string(),\n+            \"access_resource\" =\u003e \"KAPI_CAP_ACCESS_RESOURCE\".to_string(),\n+            \"perform_operation\" =\u003e \"KAPI_CAP_PERFORM_OPERATION\".to_string(),\n+            _ =\u003e action.to_string(),\n+        }\n+    }\n+\n+    /// Try to parse as JSON first\n+    fn try_parse_json(\u0026self, content: \u0026str) -\u003e Result\u003cApiSpec, serde_json::Error\u003e {\n+        let json_data: KernelApiJson = serde_json::from_str(content)?;\n+        // The kernel-side kapi_json_str() emits NULL char * as the empty\n+        // string \"\", so normalise empty -\u003e None to match the ApiSpec\n+        // convention used by --source / --vmlinux.\n+        fn opt_str(s: Option\u003cString\u003e) -\u003e Option\u003cString\u003e {\n+            s.filter(|v| !v.is_empty())\n+        }\n+        let api_type = json_data\n+            .api_type\n+            .unwrap_or_else(|| Self::api_type_from_name(\u0026json_data.name).to_string());\n+\n+        let mut spec = ApiSpec {\n+            name: json_data.name,\n+            api_type,\n+            description: opt_str(json_data.description),\n+            long_description: opt_str(json_data.long_description),\n+            version: json_data.version.map(|v| v.to_string()),\n+            context_flags: json_data\n+                .context_flags\n+                .as_deref()\n+                .map_or_else(Vec::new, Self::parse_context_flags),\n+            param_count: None,\n+            error_count: None,\n+            examples: opt_str(json_data.examples),\n+            notes: opt_str(json_data.notes),\n+            subsystem: None,   // Not in the JSON format\n+            sysfs_path: None,  // Not in the JSON format\n+            permissions: None, // Not in the JSON format\n+            capabilities: vec![],\n+            parameters: vec![],\n+            return_spec: None,\n+            errors: vec![],\n+            signals: vec![],\n+            signal_masks: vec![],\n+            side_effects: vec![],\n+            state_transitions: vec![],\n+            constraints: vec![],\n+            locks: vec![],\n+            struct_specs: vec![],\n+        };\n+\n+        // Convert capabilities\n+        if let Some(caps) = json_data.capabilities {\n+            for cap in caps {\n+                spec.capabilities.push(CapabilitySpec {\n+                    capability: cap.capability,\n+                    name: cap.name,\n+                    action: Self::parse_capability_action(\u0026cap.action),\n+                    allows: cap.allows,\n+                    without_cap: cap.without_cap,\n+                    check_condition: opt_str(cap.check_condition),\n+                    priority: cap.priority,\n+                    alternatives: cap.alternatives.unwrap_or_default(),\n+                });\n+            }\n+        }\n+\n+        // Convert parameters.\n+        if let Some(params) = json_data.parameters {\n+            for (i, p) in params.into_iter().enumerate() {\n+                let flags = p.flags.as_deref().map_or(0, Self::parse_hex_u32);\n+                let mut param = ParamSpec {\n+                    index: i as u32,\n+                    name: p.name,\n+                    type_name: p.type_name.unwrap_or_default(),\n+                    description: p.description.unwrap_or_default(),\n+                    flags,\n+                    param_type: p.type_class.as_deref().map_or(0, Self::parse_type_class),\n+                    constraint_type: p\n+                        .constraint_type\n+                        .as_deref()\n+                        .map_or(0, Self::parse_constraint_type),\n+                    constraint: opt_str(p.constraint_desc),\n+                    min_value: p.min_value,\n+                    max_value: p.max_value,\n+                    valid_mask: p.valid_mask.as_deref().map(Self::parse_hex_u64),\n+                    enum_values: p.enum_values.iter().map(i64::to_string).collect(),\n+                    size: p.size.map(|v| v as u32),\n+                    alignment: p.alignment.map(|v| v as u32),\n+                    size_param_idx: p.size_param_idx,\n+                };\n+                param.keep_used_numbers();\n+                spec.parameters.push(param);\n+            }\n+            if !spec.parameters.is_empty() {\n+                spec.param_count = Some(spec.parameters.len() as u32);\n+            }\n+        }\n+\n+        // Convert errors\n+        if let Some(errors) = json_data.errors {\n+            for e in errors {\n+                spec.errors.push(ErrorSpec {\n+                    error_code: e.error_code,\n+                    name: e.name.unwrap_or_default(),\n+                    condition: e.condition.unwrap_or_default(),\n+                    description: e.description.unwrap_or_default(),\n+                });\n+            }\n+            if !spec.errors.is_empty() {\n+                spec.error_count = Some(spec.errors.len() as u32);\n+            }\n+        }\n+\n+        // Convert return spec\n+        if let Some(ret) = json_data.return_spec {\n+            let check_type = ret.check_type.as_deref().map_or(0, Self::parse_check_type);\n+            let return_type = ret.type_class.as_deref().map_or(0, Self::parse_type_class);\n+            let type_name = ret.type_name.unwrap_or_default();\n+            let success_value = ret.success_value.unwrap_or(0);\n+            // A spec without a return block exports as an all-zero return\n+            // spec; report it as absent like the vmlinux extractor does.\n+            if !(type_name.is_empty() \u0026\u0026 return_type == 0 \u0026\u0026 check_type == 0 \u0026\u0026 success_value == 0)\n+            {\n+                let mut ret_spec = ReturnSpec {\n+                    type_name,\n+                    description: ret.description.unwrap_or_default(),\n+                    return_type,\n+                    check_type,\n+                    success_value: ret.success_value,\n+                    success_min: ret.success_min,\n+                    success_max: ret.success_max,\n+                    error_values: ret\n+                        .error_values\n+                        .into_iter()\n+                        .filter_map(|v| i32::try_from(v).ok())\n+                        .collect(),\n+                };\n+                ret_spec.keep_used_success_fields();\n+                spec.return_spec = Some(ret_spec);\n+            }\n+        }\n+\n+        // Convert locks\n+        if let Some(locks) = json_data.locks {\n+            for l in locks {\n+                let lock_type = l.lock_type.as_deref().map_or(0, Self::parse_lock_type);\n+                let scope = l.scope.as_deref().map_or(0, Self::parse_lock_scope);\n+                spec.locks.push(LockSpec {\n+                    lock_name: l.name,\n+                    lock_type,\n+                    scope,\n+                    description: l.description.unwrap_or_default(),\n+                });\n+            }\n+        }\n+\n+        // Convert constraints.  Empty strings emitted from kapi_json_str()\n+        // for NULL char * fields normalise back to None to match --source.\n+        if let Some(constraints) = json_data.constraints {\n+            for c in constraints {\n+                spec.constraints.push(ConstraintSpec {\n+                    name: c.name,\n+                    description: c.description.unwrap_or_default(),\n+                    expression: c.expression.filter(|v| !v.is_empty()),\n+                });\n+            }\n+        }\n+\n+        // Convert signals.\n+        if let Some(signals) = json_data.signals {\n+            for s in signals {\n+                let direction = s.direction.as_deref().map_or(0, Self::parse_hex_u32);\n+                let sa_flags_required = s\n+                    .sa_flags_required\n+                    .as_deref()\n+                    .map_or(0, Self::parse_hex_u32);\n+                let sa_flags_forbidden = s\n+                    .sa_flags_forbidden\n+                    .as_deref()\n+                    .map_or(0, Self::parse_hex_u32);\n+                let state_required = s.state_required.as_deref().map_or(0, Self::parse_hex_u32);\n+                let state_forbidden = s.state_forbidden.as_deref().map_or(0, Self::parse_hex_u32);\n+                let timing = s.timing.as_deref().map_or(0, Self::parse_signal_timing);\n+                spec.signals.push(super::SignalSpec {\n+                    signal_num: s.signal_num,\n+                    signal_name: s.signal_name.unwrap_or_default(),\n+                    direction,\n+                    action: s.action,\n+                    target: opt_str(s.target),\n+                    condition: opt_str(s.condition),\n+                    description: opt_str(s.description),\n+                    timing,\n+                    priority: s.priority,\n+                    restartable: s.restartable,\n+                    interruptible: s.interruptible,\n+                    queue: opt_str(s.queue_behavior),\n+                    sa_flags: 0,\n+                    sa_flags_required,\n+                    sa_flags_forbidden,\n+                    state_required,\n+                    state_forbidden,\n+                    error_on_signal: if s.error_on_signal != 0 {\n+                        Some(s.error_on_signal)\n+                    } else {\n+                        None\n+                    },\n+                    transform_to: if s.transform_to != 0 {\n+                        // Kernel JSON already carries the numeric value.\n+                        Some(s.transform_to)\n+                    } else {\n+                        None\n+                    },\n+                });\n+            }\n+        }\n+\n+        // Convert side effects.\n+        if let Some(effects) = json_data.side_effects {\n+            for e in effects {\n+                let effect_type = e.type_hex.as_deref().map_or(0, Self::parse_hex_u32);\n+                spec.side_effects.push(super::SideEffectSpec {\n+                    effect_type,\n+                    target: e.target.unwrap_or_default(),\n+                    condition: e.condition.filter(|v| !v.is_empty()),\n+                    description: e.description.unwrap_or_default(),\n+                    reversible: e.reversible,\n+                });\n+            }\n+        }\n+\n+        if let Some(transitions) = json_data.state_transitions {\n+            for t in transitions {\n+                spec.state_transitions.push(StateTransitionSpec {\n+                    object: t.object.unwrap_or_default(),\n+                    from_state: t.from_state.unwrap_or_default(),\n+                    to_state: t.to_state.unwrap_or_default(),\n+                    condition: opt_str(t.condition),\n+                    description: t.description.unwrap_or_default(),\n+                });\n+            }\n+        }\n+\n+        if let Some(masks) = json_data.signal_masks {\n+            for m in masks {\n+                spec.signal_masks.push(SignalMaskSpec {\n+                    name: m.name.unwrap_or_default(),\n+                    description: m.description.unwrap_or_default(),\n+                    signals: m.signals,\n+                });\n+            }\n+        }\n+\n+        if let Some(structs) = json_data.struct_specs {\n+            for st in structs {\n+                let fields: Vec\u003cStructFieldSpec\u003e = st\n+                    .fields\n+                    .into_iter()\n+                    .map(|f| StructFieldSpec {\n+                        name: f.name.unwrap_or_default(),\n+                        field_type: f.type_class.as_deref().map_or(0, Self::parse_type_class),\n+                        type_name: f.type_name.unwrap_or_default(),\n+                        offset: f.offset,\n+                        size: f.size,\n+                        flags: f.flags.as_deref().map_or(0, Self::parse_hex_u32),\n+                        constraint_type: f\n+                            .constraint_type\n+                            .as_deref()\n+                            .map_or(0, Self::parse_constraint_type),\n+                        min_value: f.min_value,\n+                        max_value: f.max_value,\n+                        valid_mask: f.valid_mask.as_deref().map_or(0, Self::parse_hex_u64),\n+                        description: f.description.unwrap_or_default(),\n+                    })\n+                    .collect();\n+                spec.struct_specs.push(StructSpec {\n+                    name: st.name.unwrap_or_default(),\n+                    size: st.size,\n+                    alignment: st.alignment,\n+                    field_count: fields.len() as u32,\n+                    fields,\n+                    description: st.description.unwrap_or_default(),\n+                });\n+            }\n+        }\n+\n+        Ok(spec)\n+    }\n+\n+    /// Parse a single API specification file\n+    fn parse_spec_file(\u0026self, api_name: \u0026str) -\u003e Result\u003cApiSpec\u003e {\n+        // Prefer the JSON endpoint; fall back to the plain-text dump under\n+        // kapi/specs/ if it is missing or does not parse.\n+        let json_path = self\n+            .debugfs_path\n+            .join(format!(\"kapi/specs-json/{}\", api_name));\n+        match fs::read_to_string(\u0026json_path) {\n+            Ok(content) =\u003e match self.try_parse_json(\u0026content) {\n+                Ok(spec) =\u003e return Ok(spec),\n+                Err(e) =\u003e eprintln!(\n+                    \"Warning: invalid JSON in {}: {}; using the text dump\",\n+                    json_path.display(),\n+                    e\n+                ),\n+            },\n+            Err(e) if e.kind() != std::io::ErrorKind::NotFound =\u003e eprintln!(\n+                \"Warning: cannot read {}: {}; using the text dump\",\n+                json_path.display(),\n+                e\n+            ),\n+            Err(_) =\u003e {}\n+        }\n+\n+        let spec_path = self.debugfs_path.join(format!(\"kapi/specs/{}\", api_name));\n+        let content = fs::read_to_string(\u0026spec_path)\n+            .with_context(|| format!(\"Failed to read {}\", spec_path.display()))?;\n+\n+        // The specs/ file may hold JSON as well.\n+        if let Ok(spec) = self.try_parse_json(\u0026content) {\n+            return Ok(spec);\n+        }\n+\n+        // Fall back to plain text parsing\n+        let mut spec = ApiSpec {\n+            name: api_name.to_string(),\n+            api_type: \"unknown\".to_string(),\n+            description: None,\n+            long_description: None,\n+            version: None,\n+            context_flags: Vec::new(),\n+            param_count: None,\n+            error_count: None,\n+            examples: None,\n+            notes: None,\n+            subsystem: None,\n+            sysfs_path: None,\n+            permissions: None,\n+            capabilities: vec![],\n+            parameters: vec![],\n+            return_spec: None,\n+            errors: vec![],\n+            signals: vec![],\n+            signal_masks: vec![],\n+            side_effects: vec![],\n+            state_transitions: vec![],\n+            constraints: vec![],\n+            locks: vec![],\n+            struct_specs: vec![],\n+        };\n+\n+        // Parse the content\n+        let mut text_field: Option\u003cTextField\u003e = None;\n+        let mut text_lines: Vec\u003cString\u003e = Vec::new();\n+        let mut parsing_capability = false;\n+        let mut in_capabilities_section = false;\n+        let mut current_capability: Option\u003cCapabilitySpec\u003e = None;\n+\n+        for line in content.lines() {\n+            // The kernel indents every line of a multi-line value, so a value\n+            // ends at the first non-blank line that is not indented.\n+            if let Some(field) = text_field {\n+                if line.is_empty() {\n+                    text_lines.push(String::new());\n+                    continue;\n+                }\n+                if let Some(rest) = line.strip_prefix(\"  \") {\n+                    text_lines.push(rest.to_string());\n+                    continue;\n+                }\n+                field.store(\u0026mut spec, \u0026text_lines);\n+                text_field = None;\n+            }\n+\n+            // Handle capability sections\n+            if line.starts_with(\"Capabilities (\") {\n+                in_capabilities_section = true;\n+                continue;\n+            }\n+            // Any other top-level section header ends the capabilities section\n+            // so that \"  pending_signals (0):\" inside \"Signal handling (1):\"\n+            // isn't mis-parsed as a capability entry.\n+            if !line.starts_with(' ') \u0026\u0026 !line.is_empty() \u0026\u0026 line.ends_with(':') {\n+                in_capabilities_section = false;\n+            }\n+            if in_capabilities_section\n+                \u0026\u0026 line.starts_with(\"  \")\n+                \u0026\u0026 line.contains(\" (\")\n+                \u0026\u0026 line.ends_with(\"):\")\n+            {\n+                // Start of a capability entry like \"  CAP_IPC_LOCK (14):\"\n+                if let Some(cap) = current_capability.take() {\n+                    spec.capabilities.push(cap);\n+                }\n+\n+                let parts: Vec\u003c\u0026str\u003e = line.trim().split(\" (\").collect();\n+                if parts.len() == 2 {\n+                    let cap_name = parts[0].to_string();\n+                    let cap_id = parts[1].trim_end_matches(\"):\").parse().unwrap_or(0);\n+                    current_capability = Some(CapabilitySpec {\n+                        capability: cap_id,\n+                        name: cap_name,\n+                        action: String::new(),\n+                        allows: String::new(),\n+                        without_cap: String::new(),\n+                        check_condition: None,\n+                        priority: None,\n+                        alternatives: Vec::new(),\n+                    });\n+                    parsing_capability = true;\n+                }\n+                continue;\n+            }\n+            if parsing_capability \u0026\u0026 line.starts_with(\"    \") {\n+                // Parse capability fields\n+                if let Some(ref mut cap) = current_capability {\n+                    if let Some(action) = line.strip_prefix(\"    Action: \") {\n+                        cap.action = action.to_string();\n+                    } else if let Some(allows) = line.strip_prefix(\"    Allows: \") {\n+                        cap.allows = allows.to_string();\n+                    } else if let Some(without) = line.strip_prefix(\"    Without: \") {\n+                        cap.without_cap = without.to_string();\n+                    } else if let Some(cond) = line.strip_prefix(\"    Condition: \") {\n+                        cap.check_condition = Some(cond.to_string());\n+                    } else if let Some(prio) = line.strip_prefix(\"    Priority: \") {\n+                        cap.priority = prio.parse().ok();\n+                    } else if let Some(alts) = line.strip_prefix(\"    Alternatives: \") {\n+                        cap.alternatives =\n+                            alts.split(\", \").filter_map(|s| s.parse().ok()).collect();\n+                    }\n+                }\n+                continue;\n+            }\n+            if parsing_capability \u0026\u0026 !line.starts_with(\"  \") {\n+                // End of capabilities section\n+                if let Some(cap) = current_capability.take() {\n+                    spec.capabilities.push(cap);\n+                }\n+                parsing_capability = false;\n+            }\n+\n+            // Handle section headers\n+            if line.starts_with(\"Parameters (\") {\n+                if let Some(count_str) = line\n+                    .strip_prefix(\"Parameters (\")\n+                    .and_then(|s| s.strip_suffix(\"):\"))\n+                {\n+                    spec.param_count = count_str.parse().ok();\n+                }\n+                continue;\n+            } else if line.starts_with(\"Errors (\") {\n+                if let Some(count_str) = line\n+                    .strip_prefix(\"Errors (\")\n+                    .and_then(|s| s.strip_suffix(\"):\"))\n+                {\n+                    spec.error_count = count_str.parse().ok();\n+                }\n+                continue;\n+            }\n+\n+            // Parse regular fields\n+            if let Some((field, first)) = TextField::start(line) {\n+                text_field = Some(field);\n+                text_lines.clear();\n+                if !first.is_empty() {\n+                    text_lines.push(first.to_string());\n+                }\n+            } else if let Some(version) = line.strip_prefix(\"Version: \") {\n+                spec.version = Some(version.to_string());\n+            } else if let Some(flags) = line.strip_prefix(\"Context flags: \") {\n+                spec.context_flags = flags\n+                    .split_whitespace()\n+                    .map(|f| format!(\"KAPI_CTX_{f}\"))\n+                    .collect();\n+            } else if let Some(subsys) = line.strip_prefix(\"Subsystem: \") {\n+                spec.subsystem = Some(subsys.to_string());\n+            } else if let Some(path) = line.strip_prefix(\"Sysfs Path: \") {\n+                spec.sysfs_path = Some(path.to_string());\n+            } else if let Some(perms) = line.strip_prefix(\"Permissions: \") {\n+                spec.permissions = Some(perms.to_string());\n+            }\n+        }\n+\n+        if let Some(field) = text_field {\n+            field.store(\u0026mut spec, \u0026text_lines);\n+        }\n+\n+        // Handle any remaining capability\n+        if let Some(cap) = current_capability.take() {\n+            spec.capabilities.push(cap);\n+        }\n+\n+        // Determine API type based on name\n+        if api_name.starts_with(\"sys_\") {\n+            spec.api_type = \"syscall\".to_string();\n+        } else if api_name.contains(\"_ioctl\") || api_name.starts_with(\"ioctl_\") {\n+            spec.api_type = \"ioctl\".to_string();\n+        } else if api_name.contains(\"sysfs\")\n+            || api_name.ends_with(\"_show\")\n+            || api_name.ends_with(\"_store\")\n+        {\n+            spec.api_type = \"sysfs\".to_string();\n+        } else {\n+            spec.api_type = \"function\".to_string();\n+        }\n+\n+        Ok(spec)\n+    }\n+}\n+\n+impl ApiExtractor for DebugfsExtractor {\n+    fn extract_all(\u0026self) -\u003e Result\u003cVec\u003cApiSpec\u003e\u003e {\n+        let api_names = self.parse_list_file()?;\n+        let mut specs = Vec::new();\n+\n+        for name in api_names {\n+            match self.parse_spec_file(\u0026name) {\n+                Ok(spec) =\u003e specs.push(spec),\n+                Err(e) =\u003e {\n+                    eprintln!(\"Warning: failed to parse API spec '{}': {}\", name, e);\n+                }\n+            }\n+        }\n+\n+        Ok(specs)\n+    }\n+\n+    fn extract_by_name(\u0026self, name: \u0026str) -\u003e Result\u003cOption\u003cApiSpec\u003e\u003e {\n+        let api_names = self.parse_list_file()?;\n+\n+        if api_names.contains(\u0026name.to_string()) {\n+            Ok(Some(self.parse_spec_file(name)?))\n+        } else {\n+            Ok(None)\n+        }\n+    }\n+\n+    fn display_api_details(\n+        \u0026self,\n+        api_name: \u0026str,\n+        formatter: \u0026mut dyn OutputFormatter,\n+        writer: \u0026mut dyn Write,\n+    ) -\u003e Result\u003c()\u003e {\n+        if let Some(spec) = self.extract_by_name(api_name)? {\n+            display_api_spec(\u0026spec, formatter, writer)?;\n+        } else {\n+            writeln!(writer, \"API '{api_name}' not found in debugfs\")?;\n+        }\n+\n+        Ok(())\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+\n+    // Shortened sys_read dump as emitted by kapi_export_json().\n+    const SYS_READ_JSON: \u0026str = r#\"{\n+  \"name\": \"sys_read\",\n+  \"version\": 1,\n+  \"description\": \"Read data from a file descriptor\",\n+  \"long_description\": \"\",\n+  \"context_flags\": \"0x21\",\n+  \"parameters\": [\n+    {\n+      \"name\": \"fd\",\n+      \"type\": \"unsigned int fd\",\n+      \"type_class\": \"file_descriptor\",\n+      \"flags\": \"0x1\",\n+      \"description\": \"File descriptor to read from \",\n+      \"constraint_type\": \"range\",\n+      \"constraint_desc\": \"Must be a valid, open file descriptor\",\n+      \"min_value\": 0,\n+      \"max_value\": 2147483647,\n+      \"valid_mask\": \"0x0\",\n+      \"enum_values\": [],\n+      \"size\": 0,\n+      \"alignment\": 0,\n+      \"size_param_idx\": null,\n+      \"size_multiplier\": 0\n+    },\n+    {\n+      \"name\": \"buf\",\n+      \"type\": \"char __user * buf\",\n+      \"type_class\": \"user_pointer\",\n+      \"flags\": \"0x42\",\n+      \"description\": \"User-space buffer to read data into \",\n+      \"constraint_type\": \"buffer\",\n+      \"constraint_desc\": \"Must point to a writable user-space region\",\n+      \"min_value\": 0,\n+      \"max_value\": 0,\n+      \"valid_mask\": \"0x0\",\n+      \"enum_values\": [],\n+      \"size\": 0,\n+      \"alignment\": 0,\n+      \"size_param_idx\": 2,\n+      \"size_multiplier\": 0\n+    },\n+    {\n+      \"name\": \"count\",\n+      \"type\": \"size_t count\",\n+      \"type_class\": \"uint\",\n+      \"flags\": \"0x1\",\n+      \"description\": \"Maximum number of bytes to read \",\n+      \"constraint_type\": \"none\",\n+      \"constraint_desc\": \"\",\n+      \"min_value\": 0,\n+      \"max_value\": 0,\n+      \"valid_mask\": \"0x0\",\n+      \"enum_values\": [],\n+      \"size\": 0,\n+      \"alignment\": 0,\n+      \"size_param_idx\": null,\n+      \"size_multiplier\": 0\n+    }\n+  ],\n+  \"return\": {\n+    \"type\": \"KAPI_TYPE_INT\",\n+    \"type_class\": \"int\",\n+    \"check_type\": \"range\",\n+    \"success_value\": 0,\n+    \"success_min\": 0,\n+    \"success_max\": 9223372036854775807,\n+    \"error_values\": [],\n+    \"description\": \"Number of bytes read\"\n+  },\n+  \"errors\": [\n+    { \"code\": -9, \"name\": \"EBADF\", \"condition\": \"Bad file descriptor\",\n+      \"description\": \"fd is not valid\" }\n+  ],\n+  \"locks\": [\n+    { \"name\": \"file-\u003ef_pos_lock\", \"type\": \"mutex\", \"scope\": \"internal\",\n+      \"description\": \"Position lock\" }\n+  ],\n+  \"capabilities\": [\n+    {\n+      \"capability\": 1,\n+      \"name\": \"CAP_DAC_OVERRIDE\",\n+      \"action\": \"bypass_check\",\n+      \"allows\": \"Bypass read permission checks\",\n+      \"without_cap\": \"Standard DAC checks are enforced\",\n+      \"check_condition\": \"\",\n+      \"priority\": 0\n+    }\n+  ],\n+  \"constraints\": [\n+    { \"name\": \"MAX_RW_COUNT\",\n+      \"description\": \"Count is clamped\", \"expression\": \"min(count, MAX_RW_COUNT)\" }\n+  ],\n+  \"signals\": [\n+    {\n+      \"signal_num\": 0,\n+      \"signal_name\": \"Any signal\",\n+      \"direction\": \"0x1\",\n+      \"action\": 6,\n+      \"target\": \"\",\n+      \"condition\": \"When blocked waiting for data\",\n+      \"description\": \"May be interrupted\",\n+      \"restartable\": true,\n+      \"sa_flags_required\": \"0x0\",\n+      \"sa_flags_forbidden\": \"0x0\",\n+      \"error_on_signal\": -4,\n+      \"transform_to\": 0,\n+      \"timing\": \"during\",\n+      \"priority\": 0,\n+      \"interruptible\": false,\n+      \"queue_behavior\": \"\",\n+      \"state_required\": \"0x0\",\n+      \"state_forbidden\": \"0x0\"\n+    }\n+  ],\n+  \"side_effects\": [\n+    { \"type\": \"0x10\", \"target\": \"file-\u003ef_pos\", \"condition\": \"\",\n+      \"description\": \"Offset advances\", \"reversible\": false }\n+  ],\n+  \"state_transitions\": [],\n+  \"signal_masks\": [],\n+  \"struct_specs\": [],\n+  \"examples\": \"\",\n+  \"notes\": \"\"\n+}\n+\"#;\n+\n+    // Synthetic dump exercising the fields sys_read leaves empty: enum\n+    // values, error values, struct specs, signal masks, state transitions.\n+    const FULL_JSON: \u0026str = r#\"{\n+  \"name\": \"tmp_full\",\n+  \"version\": 1,\n+  \"description\": \"Full \\\"feature\\\" test\\tline\\nbreak\\u0001\",\n+  \"long_description\": \"\",\n+  \"context_flags\": \"0x91\",\n+  \"parameters\": [\n+    {\n+      \"name\": \"mode\",\n+      \"type\": \"int mode\",\n+      \"type_class\": \"enum\",\n+      \"flags\": \"0x1\",\n+      \"description\": \"Enumerated parameter\",\n+      \"constraint_type\": \"enum\",\n+      \"constraint_desc\": \"\",\n+      \"min_value\": 0,\n+      \"max_value\": 0,\n+      \"valid_mask\": \"0x0\",\n+      \"enum_values\": [1, 2, -3, 1000000000000],\n+      \"size\": 4096,\n+      \"alignment\": 64,\n+      \"size_param_idx\": null,\n+      \"size_multiplier\": 0\n+    },\n+    {\n+      \"name\": \"bits\",\n+      \"type\": \"u64 bits\",\n+      \"type_class\": \"uint\",\n+      \"flags\": \"0x1\",\n+      \"description\": \"Masked parameter\",\n+      \"constraint_type\": \"mask\",\n+      \"constraint_desc\": \"\",\n+      \"min_value\": 0,\n+      \"max_value\": 0,\n+      \"valid_mask\": \"0xffffffffffffffff\",\n+      \"enum_values\": [],\n+      \"size\": 0,\n+      \"alignment\": 0,\n+      \"size_param_idx\": null,\n+      \"size_multiplier\": 0\n+    }\n+  ],\n+  \"return\": {\n+    \"type\": \"long\",\n+    \"type_class\": \"int\",\n+    \"check_type\": \"error_check\",\n+    \"success_value\": 0,\n+    \"success_min\": 0,\n+    \"success_max\": 0,\n+    \"error_values\": [-1, -22, -4095],\n+    \"description\": \"Error-checked return\"\n+  },\n+  \"errors\": [],\n+  \"locks\": [\n+    { \"name\": \"tmp_lock\", \"type\": \"seqlock\", \"scope\": \"acquires\", \"description\": \"a seqlock\" }\n+  ],\n+  \"capabilities\": [\n+    {\n+      \"capability\": 21,\n+      \"name\": \"CAP_SYS_ADMIN\",\n+      \"action\": \"perform_operation\",\n+      \"allows\": \"do it\",\n+      \"without_cap\": \"cannot\",\n+      \"check_condition\": \"always checked\",\n+      \"priority\": 3,\n+      \"alternatives\": [21, 17]\n+    }\n+  ],\n+  \"constraints\": [],\n+  \"signals\": [],\n+  \"side_effects\": [],\n+  \"state_transitions\": [\n+    { \"object\": \"obj\", \"from_state\": \"a\", \"to_state\": \"b\", \"condition\": \"when asked\",\n+      \"description\": \"moves a to b\" }\n+  ],\n+  \"signal_masks\": [\n+    { \"name\": \"blocked\", \"description\": \"Blocked while running\", \"signals\": [2, 15, 3] }\n+  ],\n+  \"struct_specs\": [\n+    {\n+      \"name\": \"tmp_struct\",\n+      \"size\": 16,\n+      \"alignment\": 8,\n+      \"description\": \"Test structure\",\n+      \"fields\": [\n+        {\n+          \"name\": \"flags\",\n+          \"type\": \"u32\",\n+          \"type_class\": \"uint\",\n+          \"offset\": 0,\n+          \"size\": 4,\n+          \"flags\": \"0x1\",\n+          \"constraint_type\": \"mask\",\n+          \"min_value\": 0,\n+          \"max_value\": 0,\n+          \"valid_mask\": \"0xff\",\n+          \"enum_values\": \"\",\n+          \"description\": \"Flag bits\"\n+        },\n+        {\n+          \"name\": \"kind\",\n+          \"type\": \"enum k\",\n+          \"type_class\": \"enum\",\n+          \"offset\": 8,\n+          \"size\": 4,\n+          \"flags\": \"0x0\",\n+          \"constraint_type\": \"range\",\n+          \"min_value\": -5,\n+          \"max_value\": 5,\n+          \"valid_mask\": \"0x0\",\n+          \"enum_values\": \"\",\n+          \"description\": \"Kind\"\n+        }\n+      ]\n+    }\n+  ],\n+  \"examples\": \"\",\n+  \"notes\": \"note\"\n+}\n+\"#;\n+\n+    // Dump without the optional per-parameter constraint fields.\n+    const MINIMAL_JSON: \u0026str = r#\"{\n+  \"name\": \"sys_read\",\n+  \"version\": 1,\n+  \"description\": \"Read data from a file descriptor\",\n+  \"long_description\": \"Reads.\",\n+  \"context_flags\": \"0x21\",\n+  \"parameters\": [\n+    {\n+      \"name\": \"buf\",\n+      \"type\": \"char __user * buf\",\n+      \"type_class\": \"user_pointer\",\n+      \"flags\": \"0x42\",\n+      \"description\": \"User-space buffer to read data into \"\n+    }\n+  ],\n+  \"return\": {\n+    \"type\": \"KAPI_TYPE_INT\",\n+    \"type_class\": \"int\",\n+    \"check_type\": \"range\",\n+    \"success_min\": 0,\n+    \"success_max\": 9223372036854775807,\n+    \"description\": \"Bytes read\"\n+  },\n+  \"errors\": [],\n+  \"locks\": [],\n+  \"capabilities\": [\n+    {\n+      \"capability\": 1,\n+      \"name\": \"CAP_DAC_OVERRIDE\",\n+      \"action\": \"bypass_check\",\n+      \"allows\": \"a\",\n+      \"without_cap\": \"b\",\n+      \"check_condition\": \"c\",\n+      \"priority\": 0\n+    }\n+  ],\n+  \"constraints\": [],\n+  \"signals\": [],\n+  \"side_effects\": [],\n+  \"examples\": \"\",\n+  \"notes\": \"\"\n+}\n+\"#;\n+\n+    fn extractor() -\u003e DebugfsExtractor {\n+        DebugfsExtractor {\n+            debugfs_path: PathBuf::new(),\n+        }\n+    }\n+\n+    #[test]\n+    fn json_sys_read_carries_type_and_constraint_data() {\n+        let spec = extractor().try_parse_json(SYS_READ_JSON).unwrap();\n+\n+        assert_eq!(spec.api_type, \"syscall\");\n+        assert_eq!(spec.version.as_deref(), Some(\"1\"));\n+        assert_eq!(spec.long_description, None);\n+        assert_eq!(\n+            spec.context_flags,\n+            [\"KAPI_CTX_PROCESS\", \"KAPI_CTX_SLEEPABLE\"]\n+        );\n+        assert_eq!(spec.param_count, Some(3));\n+\n+        let fd = \u0026spec.parameters[0];\n+        assert_eq!((fd.param_type, fd.constraint_type), (9, 1));\n+        assert_eq!((fd.min_value, fd.max_value), (Some(0), Some(2147483647)));\n+        assert_eq!(fd.valid_mask, None);\n+        assert_eq!(fd.size_param_idx, None);\n+        assert_eq!(\n+            fd.constraint.as_deref(),\n+            Some(\"Must be a valid, open file descriptor\")\n+        );\n+\n+        let buf = \u0026spec.parameters[1];\n+        assert_eq!((buf.param_type, buf.constraint_type), (10, 11));\n+        assert_eq!(buf.flags, 0x42);\n+        assert_eq!(buf.size_param_idx, Some(2));\n+        assert_eq!(\n+            (buf.min_value, buf.max_value, buf.valid_mask),\n+            (None, None, None)\n+        );\n+        assert_eq!((buf.size, buf.alignment), (None, None));\n+        assert_eq!(\n+            spec.parameters[buf.size_param_idx.unwrap() as usize].name,\n+            \"count\"\n+        );\n+\n+        let count = \u0026spec.parameters[2];\n+        assert_eq!((count.param_type, count.constraint_type), (2, 0));\n+        assert_eq!(count.constraint, None);\n+        assert_eq!(count.description, \"Maximum number of bytes to read \");\n+\n+        let ret = spec.return_spec.as_ref().unwrap();\n+        assert_eq!((ret.return_type, ret.check_type), (1, 1));\n+        assert_eq!(ret.success_value, None);\n+        assert_eq!(ret.success_min, Some(0));\n+        assert_eq!(ret.success_max, Some(i64::MAX));\n+        assert!(ret.error_values.is_empty());\n+\n+        assert_eq!(spec.errors[0].error_code, -9);\n+        assert_eq!((spec.locks[0].lock_type, spec.locks[0].scope), (1, 0));\n+        assert_eq!(spec.capabilities[0].action, \"KAPI_CAP_BYPASS_CHECK\");\n+        assert_eq!(spec.capabilities[0].check_condition, None);\n+        assert_eq!(spec.signals[0].timing, 1);\n+        assert_eq!(spec.signals[0].error_on_signal, Some(-4));\n+        assert_eq!(spec.signals[0].target, None);\n+        assert_eq!(spec.side_effects[0].effect_type, 0x10);\n+        assert_eq!(spec.side_effects[0].condition, None);\n+        assert_eq!(spec.constraints.len(), 1);\n+    }\n+\n+    #[test]\n+    fn json_full_carries_enum_error_struct_and_transition_data() {\n+        let spec = extractor().try_parse_json(FULL_JSON).unwrap();\n+\n+        assert_eq!(\n+            spec.description.as_deref(),\n+            Some(\"Full \\\"feature\\\" test\\tline\\nbreak\\u{1}\")\n+        );\n+        assert_eq!(\n+            spec.context_flags,\n+            [\n+                \"KAPI_CTX_PROCESS\",\n+                \"KAPI_CTX_ATOMIC\",\n+                \"KAPI_CTX_IRQ_DISABLED\"\n+            ]\n+        );\n+\n+        let mode = \u0026spec.parameters[0];\n+        assert_eq!((mode.param_type, mode.constraint_type), (6, 3));\n+        assert_eq!(mode.enum_values, [\"1\", \"2\", \"-3\", \"1000000000000\"]);\n+        assert_eq!((mode.size, mode.alignment), (Some(4096), Some(64)));\n+        assert_eq!(\n+            (mode.min_value, mode.max_value, mode.valid_mask),\n+            (None, None, None)\n+        );\n+\n+        let bits = \u0026spec.parameters[1];\n+        assert_eq!(bits.constraint_type, 2);\n+        assert_eq!(bits.valid_mask, Some(u64::MAX));\n+        assert_eq!((bits.min_value, bits.max_value), (None, None));\n+        assert_eq!((bits.size, bits.alignment), (None, None));\n+\n+        let ret = spec.return_spec.as_ref().unwrap();\n+        assert_eq!(ret.check_type, 2);\n+        assert_eq!(ret.error_values, [-1, -22, -4095]);\n+\n+        assert_eq!(spec.capabilities[0].alternatives, [21, 17]);\n+        assert_eq!(spec.capabilities[0].priority, Some(3));\n+        assert_eq!((spec.locks[0].lock_type, spec.locks[0].scope), (4, 1));\n+\n+        let trans = \u0026spec.state_transitions[0];\n+        assert_eq!(\n+            (trans.object.as_str(), trans.from_state.as_str()),\n+            (\"obj\", \"a\")\n+        );\n+        assert_eq!(trans.to_state, \"b\");\n+        assert_eq!(trans.condition.as_deref(), Some(\"when asked\"));\n+\n+        assert_eq!(spec.signal_masks[0].name, \"blocked\");\n+        assert_eq!(spec.signal_masks[0].signals, [2, 15, 3]);\n+\n+        let st = \u0026spec.struct_specs[0];\n+        assert_eq!(\n+            (st.name.as_str(), st.size, st.alignment),\n+            (\"tmp_struct\", 16, 8)\n+        );\n+        assert_eq!(st.field_count, 2);\n+        assert_eq!(st.description, \"Test structure\");\n+        assert_eq!(\n+            (st.fields[0].field_type, st.fields[0].constraint_type),\n+            (2, 2)\n+        );\n+        assert_eq!(st.fields[0].valid_mask, 0xff);\n+        assert_eq!(st.fields[1].offset, 8);\n+        assert_eq!((st.fields[1].min_value, st.fields[1].max_value), (-5, 5));\n+    }\n+\n+    #[test]\n+    fn json_without_constraint_fields_still_parses() {\n+        let spec = extractor().try_parse_json(MINIMAL_JSON).unwrap();\n+\n+        let buf = \u0026spec.parameters[0];\n+        assert_eq!(buf.param_type, 10);\n+        assert_eq!(buf.constraint_type, 0);\n+        assert_eq!(buf.min_value, None);\n+        assert_eq!(buf.valid_mask, None);\n+        assert_eq!(buf.size_param_idx, None);\n+        assert!(buf.enum_values.is_empty());\n+\n+        let ret = spec.return_spec.as_ref().unwrap();\n+        assert_eq!((ret.return_type, ret.check_type), (1, 1));\n+        assert_eq!(ret.success_value, None);\n+        assert_eq!(ret.success_max, Some(i64::MAX));\n+        assert_eq!(spec.capabilities[0].action, \"KAPI_CAP_BYPASS_CHECK\");\n+        assert!(spec.state_transitions.is_empty());\n+        assert!(spec.struct_specs.is_empty());\n+    }\n+\n+    #[test]\n+    fn enum_tokens_match_kernel_numbering() {\n+        let classes = [\n+            \"void\",\n+            \"int\",\n+            \"uint\",\n+            \"pointer\",\n+            \"struct\",\n+            \"union\",\n+            \"enum\",\n+            \"function_pointer\",\n+            \"array\",\n+            \"file_descriptor\",\n+            \"user_pointer\",\n+            \"pathname\",\n+            \"custom\",\n+        ];\n+        for (n, token) in classes.iter().enumerate() {\n+            assert_eq!(\n+                DebugfsExtractor::parse_type_class(token),\n+                n as u32,\n+                \"{token}\"\n+            );\n+        }\n+\n+        let constraints = [\n+            \"none\",\n+            \"range\",\n+            \"mask\",\n+            \"enum\",\n+            \"alignment\",\n+            \"power_of_two\",\n+            \"page_aligned\",\n+            \"nonzero\",\n+            \"user_string\",\n+            \"user_path\",\n+            \"user_ptr\",\n+            \"buffer\",\n+            \"custom\",\n+        ];\n+        for (n, token) in constraints.iter().enumerate() {\n+            assert_eq!(\n+                DebugfsExtractor::parse_constraint_type(token),\n+                n as u32,\n+                \"{token}\"\n+            );\n+        }\n+    }\n+\n+    #[test]\n+    fn api_type_follows_symbol_name() {\n+        assert_eq!(DebugfsExtractor::api_type_from_name(\"sys_read\"), \"syscall\");\n+        assert_eq!(DebugfsExtractor::api_type_from_name(\"foo_ioctl\"), \"ioctl\");\n+        assert_eq!(DebugfsExtractor::api_type_from_name(\"kmalloc\"), \"function\");\n+    }\n+\n+    #[test]\n+    fn truncated_json_is_rejected() {\n+        let cut = \u0026SYS_READ_JSON[..SYS_READ_JSON.len() / 2];\n+        assert!(extractor().try_parse_json(cut).is_err());\n+    }\n+\n+    fn debugfs_with(json: \u0026str, text: \u0026str) -\u003e tempfile::TempDir {\n+        let dir = tempfile::tempdir().unwrap();\n+        let kapi = dir.path().join(\"kapi\");\n+        fs::create_dir_all(kapi.join(\"specs\")).unwrap();\n+        fs::create_dir_all(kapi.join(\"specs-json\")).unwrap();\n+        fs::write(\n+            kapi.join(\"list\"),\n+            \"Available Kernel API Specifications\\n\\\n+             ===================================\\n\\n\\\n+             sys_read - Read data from a file descriptor\\n\\n\\\n+             Total: 1 specifications\\n\",\n+        )\n+        .unwrap();\n+        fs::write(kapi.join(\"specs-json/sys_read\"), json).unwrap();\n+        fs::write(kapi.join(\"specs/sys_read\"), text).unwrap();\n+        dir\n+    }\n+\n+    #[test]\n+    fn reads_json_endpoint_from_debugfs_tree() {\n+        let dir = debugfs_with(SYS_READ_JSON, \"Name: sys_read\\n\");\n+        let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap();\n+        let spec = ex.extract_by_name(\"sys_read\").unwrap().unwrap();\n+\n+        assert_eq!(spec.parameters[1].constraint_type, 11);\n+        assert_eq!(spec.parameters[1].size_param_idx, Some(2));\n+    }\n+\n+    #[test]\n+    fn truncated_json_endpoint_falls_back_to_text_dump() {\n+        let text = \"Name: sys_read\\n\\\n+                    Version: 1\\n\\\n+                    Description: Read data from a file descriptor\\n\\\n+                    Context flags: PROCESS SLEEPABLE \\n\";\n+        let dir = debugfs_with(\u0026SYS_READ_JSON[..SYS_READ_JSON.len() / 2], text);\n+        let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap();\n+        let spec = ex.extract_by_name(\"sys_read\").unwrap().unwrap();\n+\n+        assert!(spec.parameters.is_empty());\n+        assert_eq!(\n+            spec.context_flags,\n+            [\"KAPI_CTX_PROCESS\", \"KAPI_CTX_SLEEPABLE\"]\n+        );\n+    }\n+\n+    #[test]\n+    fn text_dump_multi_line_values_stay_in_their_field() {\n+        let text = \"Kernel API Specification\\n\\\n+                    ========================\\n\\n\\\n+                    Name: sys_read\\n\\\n+                    Version: 1\\n\\\n+                    Description: Read data from a file descriptor\\n\\\n+                    Long description:\\n  \\\n+                    First paragraph.\\n\\n  \\\n+                    Notes: not a header\\n  \\\n+                    Version: 9\\n\\\n+                    Context flags: PROCESS SLEEPABLE \\n\\n\\\n+                    Parameters (1):\\n  \\\n+                    [0] fd:\\n    \\\n+                    description: File descriptor\\n\\n\\\n+                    Examples:\\n  \\\n+                    read(fd, buf, n);\\n  \\\n+                    close(fd);\\n\\n\\\n+                    Notes:\\n  \\\n+                    one\\n\\n  \\\n+                    two\\n\\n\";\n+        let dir = debugfs_with(\"\", text);\n+        let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap();\n+        let spec = ex.extract_by_name(\"sys_read\").unwrap().unwrap();\n+\n+        assert_eq!(spec.version.as_deref(), Some(\"1\"));\n+        assert_eq!(\n+            spec.description.as_deref(),\n+            Some(\"Read data from a file descriptor\")\n+        );\n+        assert_eq!(\n+            spec.long_description.as_deref(),\n+            Some(\"First paragraph.\\n\\nNotes: not a header\\nVersion: 9\")\n+        );\n+        assert_eq!(\n+            spec.context_flags,\n+            [\"KAPI_CTX_PROCESS\", \"KAPI_CTX_SLEEPABLE\"]\n+        );\n+        assert_eq!(spec.param_count, Some(1));\n+        assert_eq!(\n+            spec.examples.as_deref(),\n+            Some(\"read(fd, buf, n);\\nclose(fd);\")\n+        );\n+        assert_eq!(spec.notes.as_deref(), Some(\"one\\n\\ntwo\"));\n+    }\n+}\ndiff --git a/tools/kapi/src/extractor/kerneldoc_parser.rs b/tools/kapi/src/extractor/kerneldoc_parser.rs\nnew file mode 100644\nindex 0000000000000..b8fae748470b0\n--- /dev/null\n+++ b/tools/kapi/src/extractor/kerneldoc_parser.rs\n@@ -0,0 +1,3335 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use super::{\n+    ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec,\n+    SideEffectSpec, SignalSpec, StateTransitionSpec,\n+};\n+use anyhow::Result;\n+use std::collections::HashMap;\n+\n+/// Kerneldoc parser that extracts KAPI annotations\n+pub struct KerneldocParser;\n+\n+/// What block are we currently inside?\n+#[derive(Debug, Clone, PartialEq)]\n+enum BlockContext {\n+    None,\n+    Param(String),   // param: \u003cname\u003e\n+    Error(String),   // error: \u003cname\u003e\n+    Signal,          // signal: \u003cname\u003e\n+    Capability,      // capability: \u003cname\u003e\n+    SideEffect,      // side-effect: \u003ctype\u003e\n+    StateTransition, // state-trans: ...\n+    Constraint,      // constraint: \u003cname\u003e\n+    Lock,            // lock: \u003cname\u003e\n+    Return,          // return:\n+}\n+\n+/// Evaluate an integer literal exactly as the compiler does on LP64 (int is\n+/// 32 bits, long and long long are 64): decimal, 0x/0X hex, 0b/0B binary or\n+/// leading-0 octal, an optional u/U/l/L suffix and an optional leading '-'.\n+/// The literal's C type (C11 6.4.4.1) decides how '-' wraps, so `-1U` is\n+/// 4294967295 and `-0x80000000` is 2147483648. The result is the 64-bit\n+/// two's complement pattern the value has once stored into an s64/u64\n+/// field. Returns `None` for anything that requires cpp-level constant\n+/// resolution (e.g. symbolic masks like `O_RDONLY | O_WRONLY`). Callers must\n+/// treat that case as \"value unknown\" and leave the downstream slot unset,\n+/// not store it as 0, which would wrongly assert that zero bits are valid.\n+fn eval_c_int_literal(s: \u0026str) -\u003e Option\u003cu64\u003e {\n+    let t = s.trim();\n+    let (neg, t) = match t.strip_prefix('-') {\n+        Some(rest) =\u003e (true, rest.trim_start()),\n+        None =\u003e (false, t),\n+    };\n+    let body = t.trim_end_matches(['u', 'U', 'l', 'L']);\n+    let suffix = \u0026t[body.len()..];\n+    let unsigned = match suffix.matches(['u', 'U']).count() {\n+        0 =\u003e false,\n+        1 =\u003e true,\n+        _ =\u003e return None,\n+    };\n+    let longs = suffix.matches(['l', 'L']).count();\n+    if longs \u003e 2 {\n+        return None;\n+    }\n+\n+    let (digits, radix) =\n+        if let Some(h) = body.strip_prefix(\"0x\").or_else(|| body.strip_prefix(\"0X\")) {\n+            (h, 16)\n+        } else if let Some(b) = body.strip_prefix(\"0b\").or_else(|| body.strip_prefix(\"0B\")) {\n+            (b, 2)\n+        } else if body.len() \u003e 1 \u0026\u0026 body.starts_with('0') {\n+            (\u0026body[1..], 8)\n+        } else {\n+            (body, 10)\n+        };\n+    if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {\n+        return None;\n+    }\n+    let v = u64::from_str_radix(digits, radix).ok()?;\n+\n+    // Decimal literals without 'u' only take signed types; GCC treats one\n+    // too large for long long as unsigned.\n+    let value = if longs == 0 \u0026\u0026 !unsigned \u0026\u0026 v \u003c= i32::MAX as u64 {\n+        let x = v as i32;\n+        (if neg { x.wrapping_neg() } else { x }) as i64 as u64\n+    } else if longs == 0 \u0026\u0026 (unsigned || radix != 10) \u0026\u0026 v \u003c= u32::MAX as u64 {\n+        let x = v as u32;\n+        u64::from(if neg { x.wrapping_neg() } else { x })\n+    } else if !unsigned \u0026\u0026 v \u003c= i64::MAX as u64 {\n+        let x = v as i64;\n+        (if neg { x.wrapping_neg() } else { x }) as u64\n+    } else if neg {\n+        v.wrapping_neg()\n+    } else {\n+        v\n+    };\n+    Some(value)\n+}\n+\n+fn parse_u64_literal(s: \u0026str) -\u003e Option\u003cu64\u003e {\n+    eval_c_int_literal(s)\n+}\n+\n+fn parse_i64_literal(s: \u0026str) -\u003e Option\u003ci64\u003e {\n+    eval_c_int_literal(s).map(|v| v as i64)\n+}\n+\n+/// Split a return `success:` value into (exact value, range minimum,\n+/// range maximum). `\u003e= N` is the range [N, i64::MAX]; `N`, `= N` and\n+/// `== N` are an exact value, and any value that is not `\u003e= N` leaves\n+/// the default range [0, i64::MAX] a range check falls back to. Mirrors\n+/// `_return_success_macro()` in kdoc_apispec.py.\n+fn parse_return_success(text: \u0026str) -\u003e (Option\u003ci64\u003e, Option\u003ci64\u003e, Option\u003ci64\u003e) {\n+    let t = text.trim();\n+    if let Some(min) = t.strip_prefix(\"\u003e=\") {\n+        return (\n+            None,\n+            Some(parse_i64_literal(min).unwrap_or(0)),\n+            Some(i64::MAX),\n+        );\n+    }\n+    let exact = t\n+        .strip_prefix(\"==\")\n+        .or_else(|| t.strip_prefix('='))\n+        .unwrap_or(t);\n+    (parse_i64_literal(exact), Some(0), Some(i64::MAX))\n+}\n+\n+/// Canonicalise a capability `type:` value to its KAPI_CAP_* spelling.\n+fn canon_kapi_cap_action(s: \u0026str) -\u003e String {\n+    let t = s.trim();\n+    if t.starts_with(\"KAPI_CAP_\") {\n+        return t.to_string();\n+    }\n+    match t.to_ascii_lowercase().as_str() {\n+        \"bypass_check\" =\u003e \"KAPI_CAP_BYPASS_CHECK\".to_string(),\n+        \"increase_limit\" =\u003e \"KAPI_CAP_INCREASE_LIMIT\".to_string(),\n+        \"override_restriction\" =\u003e \"KAPI_CAP_OVERRIDE_RESTRICTION\".to_string(),\n+        \"grant_permission\" =\u003e \"KAPI_CAP_GRANT_PERMISSION\".to_string(),\n+        \"modify_behavior\" =\u003e \"KAPI_CAP_MODIFY_BEHAVIOR\".to_string(),\n+        \"access_resource\" =\u003e \"KAPI_CAP_ACCESS_RESOURCE\".to_string(),\n+        \"perform_operation\" =\u003e \"KAPI_CAP_PERFORM_OPERATION\".to_string(),\n+        _ =\u003e t.to_string(),\n+    }\n+}\n+\n+/// Types whose semantics imply `KAPI_PARAM_USER` on the param, so\n+/// `type: user_ptr, input` doesn't need a separate `user` flag.\n+fn type_implies_user_flag(tok: \u0026str) -\u003e bool {\n+    matches!(tok.trim(), \"KAPI_TYPE_USER_PTR\" | \"KAPI_TYPE_PATH\")\n+        || matches!(\n+            tok.trim().to_ascii_lowercase().as_str(),\n+            \"user_ptr\" | \"uptr\" | \"path\"\n+        )\n+}\n+\n+/// Subfield names each block type consumes. An indented line opens a\n+/// new subfield only when it starts with one of these followed by ':';\n+/// any other line continues the previous subfield. Must stay in sync\n+/// with the `_*_SUBFIELDS` sets in tools/lib/python/kdoc/kdoc_apispec.py.\n+fn block_subfield_keys(block: \u0026BlockContext) -\u003e \u0026'static [\u0026'static str] {\n+    match block {\n+        BlockContext::Param(_) =\u003e \u0026[\n+            \"type\",\n+            \"flags\",\n+            \"size\",\n+            \"constraint-type\",\n+            \"constraint\",\n+            \"cdesc\",\n+            \"range\",\n+            \"mask\",\n+            \"valid-mask\",\n+            \"valid-values\",\n+            \"alignment\",\n+            \"size-param\",\n+            \"struct-type\",\n+            \"arch-mask\",\n+            \"desc\",\n+            \"description\",\n+        ],\n+        BlockContext::Error(_) =\u003e \u0026[\"desc\", \"condition\"],\n+        BlockContext::Signal =\u003e \u0026[\n+            \"direction\",\n+            \"action\",\n+            \"condition\",\n+            \"desc\",\n+            \"errno\",\n+            \"timing\",\n+            \"priority\",\n+            \"restartable\",\n+            \"interruptible\",\n+            \"number\",\n+            \"target\",\n+            \"queue\",\n+            \"queue_behavior\",\n+            \"transform\",\n+            \"transform_to\",\n+            \"transform-to\",\n+            \"sa_flags_required\",\n+            \"sa-flags-required\",\n+            \"sa_flags_forbidden\",\n+            \"sa-flags-forbidden\",\n+            \"state_required\",\n+            \"state-required\",\n+            \"state_forbidden\",\n+            \"state-forbidden\",\n+        ],\n+        BlockContext::Capability =\u003e \u0026[\"type\", \"allows\", \"without\", \"condition\", \"priority\", \"desc\"],\n+        BlockContext::SideEffect =\u003e \u0026[\"target\", \"desc\", \"condition\", \"reversible\"],\n+        BlockContext::StateTransition =\u003e \u0026[\"object\", \"from\", \"to\", \"condition\", \"desc\"],\n+        BlockContext::Constraint =\u003e \u0026[\"desc\", \"expr\"],\n+        BlockContext::Lock =\u003e \u0026[\n+            \"type\",\n+            \"scope\",\n+            \"acquired\",\n+            \"released\",\n+            \"held-on-entry\",\n+            \"held-on-exit\",\n+            \"desc\",\n+        ],\n+        BlockContext::Return =\u003e \u0026[\n+            \"type\",\n+            \"check-type\",\n+            \"success\",\n+            \"success-range\",\n+            \"error-values\",\n+            \"desc\",\n+        ],\n+        BlockContext::None =\u003e \u0026[],\n+    }\n+}\n+\n+/// True if `line` starts with one of `keys` followed by ':'.\n+fn starts_subfield(line: \u0026str, keys: \u0026[\u0026str]) -\u003e bool {\n+    line.split_once(':')\n+        .is_some_and(|(key, _)| keys.contains(\u0026key.trim()))\n+}\n+\n+fn is_indented_line(line: \u0026str) -\u003e bool {\n+    line.starts_with(\"  \") || line.starts_with('\\t')\n+}\n+\n+/// Fold free-form prose into paragraphs. Blank lines separate\n+/// paragraphs (\"\\n\\n\"); wrapped lines inside a paragraph are joined\n+/// with spaces, except that a line starting with \"- \" always begins a\n+/// new line so bullet lists survive. Mirrors `_fold_paragraphs()` in\n+/// kdoc_apispec.py.\n+fn fold_paragraphs(lines: \u0026[\u0026str]) -\u003e String {\n+    let mut paragraphs: Vec\u003cVec\u003cString\u003e\u003e = Vec::new();\n+    let mut current: Vec\u003cString\u003e = Vec::new();\n+    for line in lines {\n+        let line = line.trim();\n+        if line.is_empty() {\n+            if !current.is_empty() {\n+                paragraphs.push(std::mem::take(\u0026mut current));\n+            }\n+        } else if line.starts_with(\"- \") || current.is_empty() {\n+            current.push(line.to_string());\n+        } else if let Some(last) = current.last_mut() {\n+            last.push(' ');\n+            last.push_str(line);\n+        }\n+    }\n+    if !current.is_empty() {\n+        paragraphs.push(current);\n+    }\n+    paragraphs\n+        .iter()\n+        .map(|p| p.join(\"\\n\"))\n+        .collect::\u003cVec\u003c_\u003e\u003e()\n+        .join(\"\\n\\n\")\n+        .replace('\\t', \" \")\n+}\n+\n+fn expand_tabs(line: \u0026str) -\u003e String {\n+    let mut out = String::with_capacity(line.len());\n+    let mut col = 0;\n+    for c in line.chars() {\n+        if c == '\\t' {\n+            let pad = 8 - col % 8;\n+            out.extend(std::iter::repeat_n(' ', pad));\n+            col += pad;\n+        } else {\n+            out.push(c);\n+            col += 1;\n+        }\n+    }\n+    out\n+}\n+\n+/// Keep every line of a block on its own line. The indentation shared\n+/// by the continuation lines is removed so relative indentation\n+/// (nested code) is preserved; runs of blank lines collapse into one.\n+/// Mirrors `_fold_lines()` in kdoc_apispec.py.\n+fn fold_lines(lines: \u0026[\u0026str]) -\u003e String {\n+    let mut lines: Vec\u003cString\u003e = lines\n+        .iter()\n+        .map(|l| expand_tabs(l).trim_end().to_string())\n+        .collect();\n+    while lines.first().is_some_and(|l| l.is_empty()) {\n+        lines.remove(0);\n+    }\n+    while lines.last().is_some_and(|l| l.is_empty()) {\n+        lines.pop();\n+    }\n+\n+    let indent = |l: \u0026str| l.len() - l.trim_start().len();\n+    let base = lines\n+        .iter()\n+        .skip(1)\n+        .filter(|l| !l.is_empty())\n+        .map(|l| indent(l))\n+        .min()\n+        .unwrap_or(0);\n+\n+    let mut out: Vec\u003c\u0026str\u003e = Vec::new();\n+    for line in \u0026lines {\n+        if line.is_empty() {\n+            if out.last().is_some_and(|l| l.is_empty()) {\n+                continue;\n+            }\n+            out.push(\"\");\n+        } else {\n+            out.push(\u0026line[base.min(indent(line))..]);\n+        }\n+    }\n+    out.join(\"\\n\")\n+}\n+\n+impl KerneldocParser {\n+    pub fn new() -\u003e Self {\n+        KerneldocParser\n+    }\n+\n+    pub fn parse_kerneldoc(\n+        \u0026self,\n+        doc: \u0026str,\n+        name: \u0026str,\n+        api_type: \u0026str,\n+        signature: Option\u003c\u0026str\u003e,\n+    ) -\u003e Result\u003cApiSpec\u003e {\n+        let mut spec = ApiSpec {\n+            name: name.to_string(),\n+            api_type: api_type.to_string(),\n+            ..Default::default()\n+        };\n+\n+        let lines: Vec\u003c\u0026str\u003e = doc.lines().collect();\n+\n+        // Extract main description from function name line\n+        if let Some(first_line) = lines.first() {\n+            if let Some((_, desc)) = first_line.split_once(\" - \") {\n+                spec.description = Some(desc.trim().to_string());\n+            }\n+        }\n+\n+        // Extract type names from SYSCALL_DEFINE signature\n+        let type_map = if let Some(sig) = signature {\n+            self.extract_types_from_signature(sig)\n+        } else {\n+            HashMap::new()\n+        };\n+\n+        // Keep track of parameters we've seen (from @param lines)\n+        let mut param_map: HashMap\u003cString, ParamSpec\u003e = HashMap::new();\n+\n+        // Current block being parsed\n+        let mut block = BlockContext::None;\n+\n+        // Temporary storage for current block items\n+        let mut current_lock: Option\u003cLockSpec\u003e = None;\n+        let mut current_signal: Option\u003cSignalSpec\u003e = None;\n+        // Pending symbolic `transform-to:` token. Captured when the parser\n+        // sees a non-numeric value, but only reported if the final\n+        // `transform_to` after all lines in the signal block is still\n+        // unresolved. A later numeric `transform-to:` clears this so we\n+        // don't warn about a value that was subsequently overridden.\n+        let mut pending_transform_warning: Option\u003cString\u003e = None;\n+        let mut current_capability: Option\u003cCapabilitySpec\u003e = None;\n+        let mut current_side_effect: Option\u003cSideEffectSpec\u003e = None;\n+        let mut current_constraint: Option\u003cConstraintSpec\u003e = None;\n+        let mut current_error: Option\u003cErrorSpec\u003e = None;\n+        let mut current_return: Option\u003cReturnSpec\u003e = None;\n+        let mut current_state_trans: Option\u003cStateTransitionSpec\u003e = None;\n+\n+        let mut i = 0;\n+\n+        while i \u003c lines.len() {\n+            let line = lines[i];\n+            let trimmed = line.trim();\n+\n+            // Skip empty lines\n+            if trimmed.is_empty() {\n+                i += 1;\n+                continue;\n+            }\n+\n+            // Check if this is an indented continuation line (part of current block)\n+            let is_indented = is_indented_line(line);\n+\n+            // If indented and we're in a block, parse as block attribute.\n+            // A subfield line absorbs the lines that follow it until the\n+            // next known subfield key, so a value such as\n+            // `constraint-type: mask(FOO | BAR |` ... `| BAZ)` or a\n+            // wrapped `condition:` arrives as a single logical line.\n+            if is_indented \u0026\u0026 block != BlockContext::None {\n+                let keys = block_subfield_keys(\u0026block);\n+                let mut logical = trimmed.to_string();\n+                if starts_subfield(trimmed, keys) {\n+                    let mut j = i + 1;\n+                    while j \u003c lines.len() {\n+                        let next = lines[j];\n+                        let next_trim = next.trim();\n+                        if next_trim.is_empty() {\n+                            j += 1;\n+                            continue;\n+                        }\n+                        if !is_indented_line(next) || starts_subfield(next_trim, keys) {\n+                            break;\n+                        }\n+                        logical.push(' ');\n+                        logical.push_str(next_trim);\n+                        i = j;\n+                        j += 1;\n+                    }\n+                }\n+                self.parse_block_attribute(\n+                    \u0026logical,\n+                    \u0026block,\n+                    \u0026mut param_map,\n+                    \u0026mut current_error,\n+                    \u0026mut current_signal,\n+                    \u0026mut pending_transform_warning,\n+                    \u0026mut current_capability,\n+                    \u0026mut current_side_effect,\n+                    \u0026mut current_constraint,\n+                    \u0026mut current_lock,\n+                    \u0026mut current_return,\n+                    \u0026mut current_state_trans,\n+                );\n+                i += 1;\n+                continue;\n+            }\n+\n+            // Not indented or not in block: flush current block if any.\n+            // If a symbolic `transform-to:` was captured and no later\n+            // numeric line cleared it, surface the warning now; by\n+            // construction `transform_to` is None in that case.\n+            if matches!(block, BlockContext::Signal) {\n+                if let Some(raw) = pending_transform_warning.take() {\n+                    eprintln!(\n+                        \"kapi: warning: transform-to: {raw:?} is symbolic; \\\n+                         source-mode cannot resolve signal numbers portably. \\\n+                         Use --vmlinux or --debugfs to get the resolved value.\",\n+                    );\n+                }\n+            }\n+            self.flush_block(\n+                \u0026mut block,\n+                \u0026mut spec,\n+                \u0026mut current_error,\n+                \u0026mut current_signal,\n+                \u0026mut current_capability,\n+                \u0026mut current_side_effect,\n+                \u0026mut current_constraint,\n+                \u0026mut current_lock,\n+                \u0026mut current_return,\n+                \u0026mut current_state_trans,\n+            );\n+\n+            // Parse top-level annotations\n+            if let Some(rest) = trimmed.strip_prefix(\"@\") {\n+                // @param: description (standard kerneldoc parameter)\n+                if let Some((param_name, desc)) = rest.split_once(':') {\n+                    let param_name = param_name.trim();\n+                    let desc = desc.trim();\n+                    if !param_name.contains('-') {\n+                        let idx = param_map.len() as u32;\n+                        let type_name = type_map.get(param_name).cloned().unwrap_or_default();\n+                        param_map.insert(\n+                            param_name.to_string(),\n+                            ParamSpec {\n+                                index: idx,\n+                                name: param_name.to_string(),\n+                                type_name,\n+                                description: desc.to_string(),\n+                                flags: 0,\n+                                param_type: 0,\n+                                constraint_type: 0,\n+                                constraint: None,\n+                                min_value: None,\n+                                max_value: None,\n+                                valid_mask: None,\n+                                enum_values: vec![],\n+                                size: None,\n+                                alignment: None,\n+                                size_param_idx: None,\n+                            },\n+                        );\n+                    }\n+                }\n+            } else if let Some(rest) = trimmed.strip_prefix(\"long-desc:\") {\n+                let (section, next_i) = self.collect_section(\u0026lines, i, rest);\n+                let val = fold_paragraphs(\u0026section);\n+                spec.long_description = Some(val).filter(|v| !v.is_empty());\n+                i = next_i;\n+                continue;\n+            } else if let Some(rest) = trimmed.strip_prefix(\"context-flags:\") {\n+                spec.context_flags = self.parse_context_flags(rest.trim());\n+            } else if let Some(rest) = trimmed.strip_prefix(\"contexts:\") {\n+                // Short form: \"contexts: process, sleepable\"\n+                spec.context_flags = self.parse_context_list(rest.trim());\n+            } else if let Some(rest) = trimmed.strip_prefix(\"param-count:\") {\n+                spec.param_count = rest.trim().parse().ok();\n+            }\n+            // Block-start annotations\n+            else if let Some(rest) = trimmed.strip_prefix(\"param:\") {\n+                let param_name = rest.trim().to_string();\n+                block = BlockContext::Param(param_name.clone());\n+                // Ensure param exists in map\n+                if !param_map.contains_key(\u0026param_name) {\n+                    let idx = param_map.len() as u32;\n+                    let type_name = type_map\n+                        .get(param_name.as_str())\n+                        .cloned()\n+                        .unwrap_or_default();\n+                    param_map.insert(\n+                        param_name.clone(),\n+                        ParamSpec {\n+                            index: idx,\n+                            name: param_name,\n+                            type_name,\n+                            description: String::new(),\n+                            flags: 0,\n+                            param_type: 0,\n+                            constraint_type: 0,\n+                            constraint: None,\n+                            min_value: None,\n+                            max_value: None,\n+                            valid_mask: None,\n+                            enum_values: vec![],\n+                            size: None,\n+                            alignment: None,\n+                            size_param_idx: None,\n+                        },\n+                    );\n+                }\n+            } else if let Some(rest) = trimmed.strip_prefix(\"error:\") {\n+                // error: NAME, condition\n+                let parts: Vec\u003c\u0026str\u003e = rest.splitn(2, ',').map(|s| s.trim()).collect();\n+                if !parts.is_empty() {\n+                    let error_name = parts[0].to_string();\n+                    let condition = if parts.len() \u003e= 2 {\n+                        parts[1].to_string()\n+                    } else {\n+                        String::new()\n+                    };\n+                    let error_code = self.error_name_to_code(\u0026error_name);\n+                    current_error = Some(ErrorSpec {\n+                        error_code,\n+                        name: error_name.clone(),\n+                        condition,\n+                        description: String::new(),\n+                    });\n+                    block = BlockContext::Error(error_name);\n+                }\n+            } else if let Some(rest) = trimmed.strip_prefix(\"signal:\") {\n+                let signal_name = rest.trim().to_string();\n+                current_signal = Some(SignalSpec {\n+                    signal_num: 0,\n+                    signal_name,\n+                    direction: 1,\n+                    action: 0,\n+                    target: None,\n+                    condition: None,\n+                    description: None,\n+                    restartable: false,\n+                    timing: 0,\n+                    priority: 0,\n+                    interruptible: false,\n+                    queue: None,\n+                    sa_flags: 0,\n+                    sa_flags_required: 0,\n+                    sa_flags_forbidden: 0,\n+                    state_required: 0,\n+                    state_forbidden: 0,\n+                    error_on_signal: None,\n+                    transform_to: None,\n+                });\n+                block = BlockContext::Signal;\n+            } else if let Some(rest) = trimmed.strip_prefix(\"capability:\") {\n+                let parts: Vec\u003c\u0026str\u003e = rest.split(',').map(|s| s.trim()).collect();\n+                if !parts.is_empty() {\n+                    let cap_name = parts[0].to_string();\n+                    let cap_value = self.parse_capability_value(\u0026cap_name);\n+                    // If we have 3 parts, it's flat format: capability: CAP, action, name\n+                    let (action, name) = if parts.len() \u003e= 3 {\n+                        (parts[1].to_string(), parts[2].to_string())\n+                    } else {\n+                        (String::new(), cap_name.clone())\n+                    };\n+                    current_capability = Some(CapabilitySpec {\n+                        capability: cap_value,\n+                        name,\n+                        action,\n+                        allows: String::new(),\n+                        without_cap: String::new(),\n+                        check_condition: None,\n+                        priority: Some(0),\n+                        alternatives: vec![],\n+                    });\n+                    block = BlockContext::Capability;\n+                }\n+            } else if let Some(rest) = trimmed.strip_prefix(\"side-effect:\") {\n+                // Could be flat format (comma-separated) or block start\n+                let rest = rest.trim();\n+                // Check if it's the flat format with commas\n+                let comma_parts: Vec\u003c\u0026str\u003e = rest.splitn(3, ',').map(|s| s.trim()).collect();\n+                if comma_parts.len() \u003e= 3 {\n+                    // Flat format: side-effect: TYPE, target, desc\n+                    let mut effect = SideEffectSpec {\n+                        effect_type: self.parse_effect_type(comma_parts[0]),\n+                        target: comma_parts[1].to_string(),\n+                        condition: None,\n+                        description: comma_parts[2].to_string(),\n+                        reversible: false,\n+                    };\n+                    if comma_parts[2].contains(\"reversible=yes\") {\n+                        effect.reversible = true;\n+                    }\n+                    spec.side_effects.push(effect);\n+                } else {\n+                    // Block format: side-effect: TYPE\n+                    current_side_effect = Some(SideEffectSpec {\n+                        effect_type: self.parse_effect_type(rest),\n+                        target: String::new(),\n+                        condition: None,\n+                        description: String::new(),\n+                        reversible: false,\n+                    });\n+                    block = BlockContext::SideEffect;\n+                }\n+            } else if let Some(rest) = trimmed.strip_prefix(\"state-trans:\") {\n+                // Flat form: state-trans: OBJECT, FROM, TO, DESCRIPTION\n+                // (the description may itself contain commas). Block\n+                // form: a bare OBJECT followed by from:/to:/condition:/\n+                // desc: subfields.\n+                if rest.contains(',') {\n+                    let mut parts = rest.splitn(4, ',').map(|s| s.trim());\n+                    let mut next_part = || parts.next().unwrap_or_default().to_string();\n+                    spec.state_transitions.push(StateTransitionSpec {\n+                        object: next_part(),\n+                        from_state: next_part(),\n+                        to_state: next_part(),\n+                        condition: None,\n+                        description: next_part(),\n+                    });\n+                } else {\n+                    current_state_trans = Some(StateTransitionSpec {\n+                        object: rest.trim().to_string(),\n+                        from_state: String::new(),\n+                        to_state: String::new(),\n+                        condition: None,\n+                        description: String::new(),\n+                    });\n+                }\n+                block = BlockContext::StateTransition;\n+            } else if let Some(rest) = trimmed.strip_prefix(\"constraint:\") {\n+                let rest = rest.trim();\n+                // Could be flat format: constraint: name, desc\n+                // Or block format: constraint: name\n+                let parts: Vec\u003c\u0026str\u003e = rest.splitn(2, ',').map(|s| s.trim()).collect();\n+                if parts.len() \u003e= 2 {\n+                    // Flat format\n+                    current_constraint = Some(ConstraintSpec {\n+                        name: parts[0].to_string(),\n+                        description: parts[1].to_string(),\n+                        expression: None,\n+                    });\n+                } else {\n+                    // Block format\n+                    current_constraint = Some(ConstraintSpec {\n+                        name: rest.to_string(),\n+                        description: String::new(),\n+                        expression: None,\n+                    });\n+                }\n+                block = BlockContext::Constraint;\n+            } else if let Some(rest) = trimmed.strip_prefix(\"lock:\") {\n+                let rest = rest.trim();\n+                // Could be flat: lock: name, type\n+                // Or block: lock: name\n+                let parts: Vec\u003c\u0026str\u003e = rest.split(',').map(|s| s.trim()).collect();\n+                if parts.len() \u003e= 2 {\n+                    current_lock = Some(LockSpec {\n+                        lock_name: parts[0].to_string(),\n+                        lock_type: self.parse_lock_type(parts[1]),\n+                        scope: super::KAPI_LOCK_INTERNAL,\n+                        description: String::new(),\n+                    });\n+                } else {\n+                    current_lock = Some(LockSpec {\n+                        lock_name: rest.to_string(),\n+                        lock_type: 0,\n+                        scope: super::KAPI_LOCK_INTERNAL,\n+                        description: String::new(),\n+                    });\n+                }\n+                block = BlockContext::Lock;\n+            }\n+            // Other top-level annotations\n+            else if let Some(rest) = trimmed.strip_prefix(\"return:\") {\n+                let rest = rest.trim();\n+                if rest.is_empty() {\n+                    // Block format\n+                    current_return = Some(ReturnSpec {\n+                        type_name: String::new(),\n+                        description: String::new(),\n+                        return_type: 0,\n+                        check_type: 0,\n+                        success_value: None,\n+                        success_min: None,\n+                        success_max: None,\n+                        error_values: vec![],\n+                    });\n+                    block = BlockContext::Return;\n+                }\n+            } else if let Some(rest) = trimmed.strip_prefix(\"examples:\") {\n+                let (section, next_i) = self.collect_section(\u0026lines, i, rest);\n+                let val = fold_lines(\u0026section);\n+                spec.examples = Some(val).filter(|v| !v.is_empty());\n+                i = next_i;\n+                continue;\n+            } else if let Some(rest) = trimmed.strip_prefix(\"notes:\") {\n+                let (section, next_i) = self.collect_section(\u0026lines, i, rest);\n+                let val = fold_paragraphs(\u0026section);\n+                spec.notes = Some(val).filter(|v| !v.is_empty());\n+                i = next_i;\n+                continue;\n+            }\n+\n+            i += 1;\n+        }\n+\n+        // Flush any remaining block. Emit a pending symbolic\n+        // `transform-to:` warning if the final state still has no\n+        // resolved numeric value (see per-line loop for rationale).\n+        if matches!(block, BlockContext::Signal) {\n+            if let Some(raw) = pending_transform_warning.take() {\n+                eprintln!(\n+                    \"kapi: warning: transform-to: {raw:?} is symbolic; \\\n+                     source-mode cannot resolve signal numbers portably. \\\n+                     Use --vmlinux or --debugfs to get the resolved value.\",\n+                );\n+            }\n+        }\n+        self.flush_block(\n+            \u0026mut block,\n+            \u0026mut spec,\n+            \u0026mut current_error,\n+            \u0026mut current_signal,\n+            \u0026mut current_capability,\n+            \u0026mut current_side_effect,\n+            \u0026mut current_constraint,\n+            \u0026mut current_lock,\n+            \u0026mut current_return,\n+            \u0026mut current_state_trans,\n+        );\n+\n+        // Convert param_map to vec preserving order\n+        let mut params: Vec\u003cParamSpec\u003e = param_map.into_values().collect();\n+        params.sort_by_key(|p| p.index);\n+\n+        // If the spec carries an explicit param-count, warn when it\n+        // disagrees with the number of param: blocks we actually saw.\n+        if let Some(claimed) = spec.param_count {\n+            if claimed as usize != params.len() {\n+                eprintln!(\n+                    \"kapi: {}: param-count: {} disagrees with {} param: block(s)\",\n+                    name,\n+                    claimed,\n+                    params.len(),\n+                );\n+            }\n+        }\n+\n+        for param in \u0026mut params {\n+            param.drop_unset_string_limits();\n+        }\n+        spec.parameters = params;\n+\n+        Ok(spec)\n+    }\n+\n+    /// Parse an indented attribute line within a block\n+    #[allow(clippy::too_many_arguments)]\n+    fn parse_block_attribute(\n+        \u0026self,\n+        trimmed: \u0026str,\n+        block: \u0026BlockContext,\n+        param_map: \u0026mut HashMap\u003cString, ParamSpec\u003e,\n+        current_error: \u0026mut Option\u003cErrorSpec\u003e,\n+        current_signal: \u0026mut Option\u003cSignalSpec\u003e,\n+        pending_transform_warning: \u0026mut Option\u003cString\u003e,\n+        current_capability: \u0026mut Option\u003cCapabilitySpec\u003e,\n+        current_side_effect: \u0026mut Option\u003cSideEffectSpec\u003e,\n+        current_constraint: \u0026mut Option\u003cConstraintSpec\u003e,\n+        current_lock: \u0026mut Option\u003cLockSpec\u003e,\n+        current_return: \u0026mut Option\u003cReturnSpec\u003e,\n+        current_state_trans: \u0026mut Option\u003cStateTransitionSpec\u003e,\n+    ) {\n+        match block {\n+            BlockContext::Param(param_name) =\u003e {\n+                if let Some(param) = param_map.get_mut(param_name) {\n+                    if let Some(rest) = trimmed.strip_prefix(\"type:\") {\n+                        // Accept either:\n+                        //   type: KAPI_TYPE_UINT              (long, single token)\n+                        //   type: uint                        (short, single token)\n+                        //   type: uint, input                 (short, type + flags)\n+                        //   type: path, input                 (short, type + flags)\n+                        // Single-token inputs leave flags alone; they are\n+                        // set by a separate `flags:` line.\n+                        //\n+                        // User-space pointer types (user_ptr, path) imply\n+                        // KAPI_PARAM_USER, so specs don't need to repeat\n+                        // `user` after the type.\n+                        let mut parts = rest.split(',').map(str::trim);\n+                        let type_token = parts.next();\n+                        if let Some(ty) = type_token {\n+                            param.param_type = self.parse_param_type(ty);\n+                        }\n+                        for flag in parts {\n+                            param.flags |= self.parse_param_flag_token(flag);\n+                        }\n+                        if type_token.map(type_implies_user_flag).unwrap_or(false) {\n+                            param.flags |= 1 \u003c\u003c 6; // KAPI_PARAM_USER\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"flags:\") {\n+                        param.flags = self.parse_param_flags(rest.trim());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"constraint-type:\") {\n+                        // Accepts `KAPI_CONSTRAINT_*` enum tokens or\n+                        // function-call expressions like `range(0, 4096)`\n+                        // / `mask(0xff)` / `buffer(2)` that also populate\n+                        // the matching numeric fields on `param`.\n+                        let text = rest.trim();\n+                        if !self.apply_constraint_expr(param, text) {\n+                            param.constraint_type = self.parse_constraint_type(text);\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"valid-mask:\") {\n+                        // Symbolic mask values need cpp-level resolution;\n+                        // leave that to the binary reader.\n+                        let _ = rest;\n+                    } else if let Some(rest) = trimmed\n+                        .strip_prefix(\"cdesc:\")\n+                        .or_else(|| trimmed.strip_prefix(\"constraint:\"))\n+                    {\n+                        // Free-text constraint description.\n+                        param.constraint = Some(rest.trim().to_string());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"range:\") {\n+                        let parts: Vec\u003c\u0026str\u003e = rest.split(',').map(|s| s.trim()).collect();\n+                        if parts.len() \u003e= 2 {\n+                            param.min_value = parts[0].parse().ok();\n+                            param.max_value = parts[1].parse().ok();\n+                            param.constraint_type = 1; // KAPI_CONSTRAINT_RANGE\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"size-param:\") {\n+                        param.size_param_idx = rest.trim().parse().ok();\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"description:\") {\n+                        param.description = rest.trim().to_string();\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"desc:\") {\n+                        param.description = rest.trim().to_string();\n+                    } else if !trimmed.contains(':') || trimmed.starts_with(\"  \") {\n+                        // Continuation of the previous attribute's value.\n+                        if let Some(c) = param.constraint.as_mut() {\n+                            c.push(' ');\n+                            c.push_str(trimmed);\n+                        }\n+                    }\n+                }\n+            }\n+            BlockContext::Error(_) =\u003e {\n+                if let Some(error) = current_error.as_mut() {\n+                    if let Some(rest) = trimmed.strip_prefix(\"desc:\") {\n+                        let text = rest.trim().to_string();\n+                        if error.description.is_empty() {\n+                            error.description = text;\n+                        } else {\n+                            error.description.push(' ');\n+                            error.description.push_str(\u0026text);\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"condition:\") {\n+                        error.condition = rest.trim().to_string();\n+                    } else {\n+                        // Continuation of description\n+                        if !error.description.is_empty() {\n+                            error.description.push(' ');\n+                            error.description.push_str(trimmed);\n+                        }\n+                    }\n+                }\n+            }\n+            BlockContext::Signal =\u003e {\n+                if let Some(signal) = current_signal.as_mut() {\n+                    if let Some(rest) = trimmed.strip_prefix(\"direction:\") {\n+                        signal.direction = self.parse_signal_direction(rest.trim());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"action:\") {\n+                        signal.action = self.parse_signal_action(rest.trim());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"condition:\") {\n+                        signal.condition = Some(rest.trim().to_string());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"desc:\") {\n+                        let text = rest.trim().to_string();\n+                        if signal.description.is_none() {\n+                            signal.description = Some(text);\n+                        } else if let Some(d) = signal.description.as_mut() {\n+                            d.push(' ');\n+                            d.push_str(\u0026text);\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"errno:\") {\n+                        // `error:` cannot be used here because kerneldoc\n+                        // promotes it to a top-level section header.\n+                        //\n+                        // Accepted forms:\n+                        //   errno: -4         -\u003e numeric literal, stored as-is\n+                        //   errno: -EINTR     -\u003e kernel convention; resolve\n+                        //                       the symbol and negate\n+                        //   errno: EINTR      -\u003e bare symbol; resolved value\n+                        //                       is already negative\n+                        let value = rest.trim();\n+                        signal.error_on_signal = if let Ok(code) = value.parse::\u003ci32\u003e() {\n+                            Some(code)\n+                        } else if let Some(name) = value.strip_prefix('-') {\n+                            // `error_name_to_code` already returns the negated\n+                            // code (e.g. \"EINTR\" -\u003e -4), so `-EINTR` resolves\n+                            // to -4 too; the leading `-` on the symbolic form\n+                            // is kernel-source convention, not a second negation.\n+                            Some(self.error_name_to_code(name))\n+                        } else {\n+                            Some(self.error_name_to_code(value))\n+                        };\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"timing:\") {\n+                        signal.timing = self.parse_signal_timing(rest.trim());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"restartable:\") {\n+                        let val = rest.trim().to_lowercase();\n+                        signal.restartable = matches!(val.as_str(), \"yes\" | \"true\" | \"1\");\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"interruptible:\") {\n+                        let val = rest.trim().to_lowercase();\n+                        signal.interruptible = matches!(val.as_str(), \"yes\" | \"true\" | \"1\");\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"priority:\") {\n+                        signal.priority = rest.trim().parse().unwrap_or(0);\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"target:\") {\n+                        signal.target = Some(rest.trim().to_string());\n+                    } else if let Some(rest) = trimmed\n+                        .strip_prefix(\"queue:\")\n+                        .or_else(|| trimmed.strip_prefix(\"queue_behavior:\"))\n+                    {\n+                        signal.queue = Some(rest.trim().to_string());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"number:\") {\n+                        signal.signal_num = rest.trim().parse().unwrap_or(0);\n+                    } else if let Some(rest) = trimmed\n+                        .strip_prefix(\"transform-to:\")\n+                        .or_else(|| trimmed.strip_prefix(\"transform_to:\"))\n+                        .or_else(|| trimmed.strip_prefix(\"transform:\"))\n+                    {\n+                        // transform-to: takes a signal constant (e.g.\n+                        // SIGKILL) or a numeric literal. Only a numeric\n+                        // literal fills `transform_to`; symbolic values\n+                        // cannot be resolved portably in userspace\n+                        // because signal numbers are arch-dependent and\n+                        // we have no access to the target arch's\n+                        // \u003casm/signal.h\u003e. Report such cases to stderr so\n+                        // they are not silently lost, and point the user\n+                        // at --vmlinux / --debugfs, which consult the\n+                        // compiled struct where the C preprocessor has\n+                        // already baked in the correct value.\n+                        //\n+                        // Assign unconditionally so the last line in\n+                        // the kerneldoc wins and an intended symbolic\n+                        // override doesn't silently leave a stale\n+                        // numeric value from an earlier line. The\n+                        // warning is deferred until flush_block() so a\n+                        // subsequent numeric line can cancel it; if the\n+                        // last line was still symbolic we report it\n+                        // then.\n+                        let v = rest.trim();\n+                        let parsed = v.parse::\u003ci32\u003e().ok();\n+                        signal.transform_to = parsed;\n+                        if parsed.is_some() {\n+                            *pending_transform_warning = None;\n+                        } else if !v.is_empty() {\n+                            *pending_transform_warning = Some(v.to_string());\n+                        }\n+                    } else if let Some(rest) = trimmed\n+                        .strip_prefix(\"sa-flags-required:\")\n+                        .or_else(|| trimmed.strip_prefix(\"sa_flags_required:\"))\n+                    {\n+                        signal.sa_flags_required = self.parse_hex_or_bitmask(rest.trim());\n+                    } else if let Some(rest) = trimmed\n+                        .strip_prefix(\"sa-flags-forbidden:\")\n+                        .or_else(|| trimmed.strip_prefix(\"sa_flags_forbidden:\"))\n+                    {\n+                        signal.sa_flags_forbidden = self.parse_hex_or_bitmask(rest.trim());\n+                    } else if let Some(rest) = trimmed\n+                        .strip_prefix(\"state-required:\")\n+                        .or_else(|| trimmed.strip_prefix(\"state_required:\"))\n+                    {\n+                        signal.state_required = self.parse_signal_state_mask(rest.trim());\n+                    } else if let Some(rest) = trimmed\n+                        .strip_prefix(\"state-forbidden:\")\n+                        .or_else(|| trimmed.strip_prefix(\"state_forbidden:\"))\n+                    {\n+                        signal.state_forbidden = self.parse_signal_state_mask(rest.trim());\n+                    } else {\n+                        // Continuation of description\n+                        if let Some(d) = signal.description.as_mut() {\n+                            d.push(' ');\n+                            d.push_str(trimmed);\n+                        }\n+                    }\n+                }\n+            }\n+            BlockContext::Capability =\u003e {\n+                if let Some(cap) = current_capability.as_mut() {\n+                    if let Some(rest) = trimmed.strip_prefix(\"type:\") {\n+                        cap.action = canon_kapi_cap_action(rest.trim());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"allows:\") {\n+                        cap.allows = rest.trim().to_string();\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"without:\") {\n+                        cap.without_cap = rest.trim().to_string();\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"condition:\") {\n+                        cap.check_condition = Some(rest.trim().to_string());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"priority:\") {\n+                        cap.priority = rest.trim().parse().ok();\n+                    }\n+                }\n+            }\n+            BlockContext::SideEffect =\u003e {\n+                if let Some(effect) = current_side_effect.as_mut() {\n+                    if let Some(rest) = trimmed.strip_prefix(\"target:\") {\n+                        effect.target = rest.trim().to_string();\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"condition:\") {\n+                        effect.condition = Some(rest.trim().to_string());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"desc:\") {\n+                        let text = rest.trim().to_string();\n+                        if effect.description.is_empty() {\n+                            effect.description = text;\n+                        } else {\n+                            effect.description.push(' ');\n+                            effect.description.push_str(\u0026text);\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"reversible:\") {\n+                        let val = rest.trim().to_lowercase();\n+                        effect.reversible = matches!(val.as_str(), \"yes\" | \"true\" | \"1\");\n+                    } else {\n+                        // Continuation of description\n+                        if !effect.description.is_empty() {\n+                            effect.description.push(' ');\n+                            effect.description.push_str(trimmed);\n+                        }\n+                    }\n+                }\n+            }\n+            BlockContext::Constraint =\u003e {\n+                if let Some(constraint) = current_constraint.as_mut() {\n+                    if let Some(rest) = trimmed.strip_prefix(\"desc:\") {\n+                        let text = rest.trim().to_string();\n+                        if constraint.description.is_empty() {\n+                            constraint.description = text;\n+                        } else {\n+                            constraint.description.push(' ');\n+                            constraint.description.push_str(\u0026text);\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"expr:\") {\n+                        constraint.expression = Some(rest.trim().to_string());\n+                    } else {\n+                        // Continuation of description\n+                        if !constraint.description.is_empty() {\n+                            constraint.description.push(' ');\n+                            constraint.description.push_str(trimmed);\n+                        }\n+                    }\n+                }\n+            }\n+            BlockContext::Lock =\u003e {\n+                if let Some(lock) = current_lock.as_mut() {\n+                    if let Some(rest) = trimmed.strip_prefix(\"type:\") {\n+                        lock.lock_type = self.parse_lock_type(rest.trim());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"scope:\") {\n+                        lock.scope = match rest.trim() {\n+                            \"internal\" =\u003e super::KAPI_LOCK_INTERNAL,\n+                            \"acquires\" =\u003e super::KAPI_LOCK_ACQUIRES,\n+                            \"releases\" =\u003e super::KAPI_LOCK_RELEASES,\n+                            \"caller_held\" =\u003e super::KAPI_LOCK_CALLER_HELD,\n+                            _ =\u003e super::KAPI_LOCK_INTERNAL,\n+                        };\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"desc:\") {\n+                        let text = rest.trim().to_string();\n+                        if lock.description.is_empty() {\n+                            lock.description = text;\n+                        } else {\n+                            lock.description.push(' ');\n+                            lock.description.push_str(\u0026text);\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"acquired:\") {\n+                        // Same rule as kdoc_apispec.py: a lock that is both\n+                        // acquired and released stays KAPI_LOCK_INTERNAL.\n+                        if matches!(rest.trim().to_lowercase().as_str(), \"true\" | \"yes\") {\n+                            lock.scope = if lock.scope == super::KAPI_LOCK_RELEASES {\n+                                super::KAPI_LOCK_INTERNAL\n+                            } else {\n+                                super::KAPI_LOCK_ACQUIRES\n+                            };\n+                        }\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"released:\") {\n+                        if matches!(rest.trim().to_lowercase().as_str(), \"true\" | \"yes\") {\n+                            lock.scope = if lock.scope == super::KAPI_LOCK_ACQUIRES {\n+                                super::KAPI_LOCK_INTERNAL\n+                            } else {\n+                                super::KAPI_LOCK_RELEASES\n+                            };\n+                        }\n+                    } else {\n+                        // Continuation of description\n+                        if !lock.description.is_empty() {\n+                            lock.description.push(' ');\n+                            lock.description.push_str(trimmed);\n+                        }\n+                    }\n+                }\n+            }\n+            BlockContext::Return =\u003e {\n+                if let Some(ret) = current_return.as_mut() {\n+                    if let Some(rest) = trimmed.strip_prefix(\"type:\") {\n+                        let raw = rest.trim();\n+                        ret.type_name = raw.to_string();\n+                        ret.return_type = self.parse_param_type(raw);\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"check-type:\") {\n+                        ret.check_type = self.parse_return_check_type(rest.trim());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"success:\") {\n+                        (ret.success_value, ret.success_min, ret.success_max) =\n+                            parse_return_success(rest.trim());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"desc:\") {\n+                        let text = rest.trim().to_string();\n+                        if ret.description.is_empty() {\n+                            ret.description = text;\n+                        } else {\n+                            ret.description.push(' ');\n+                            ret.description.push_str(\u0026text);\n+                        }\n+                    } else {\n+                        // Continuation of description\n+                        if !ret.description.is_empty() {\n+                            ret.description.push(' ');\n+                            ret.description.push_str(trimmed);\n+                        }\n+                    }\n+                }\n+            }\n+            BlockContext::StateTransition =\u003e {\n+                if let Some(trans) = current_state_trans.as_mut() {\n+                    if let Some(rest) = trimmed.strip_prefix(\"object:\") {\n+                        trans.object = rest.trim().to_string();\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"from:\") {\n+                        trans.from_state = rest.trim().to_string();\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"to:\") {\n+                        trans.to_state = rest.trim().to_string();\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"condition:\") {\n+                        trans.condition = Some(rest.trim().to_string());\n+                    } else if let Some(rest) = trimmed.strip_prefix(\"desc:\") {\n+                        trans.description = rest.trim().to_string();\n+                    }\n+                }\n+            }\n+            BlockContext::None =\u003e {}\n+        }\n+    }\n+\n+    /// Flush the current block, pushing items into the spec\n+    #[allow(clippy::too_many_arguments)]\n+    fn flush_block(\n+        \u0026self,\n+        block: \u0026mut BlockContext,\n+        spec: \u0026mut ApiSpec,\n+        current_error: \u0026mut Option\u003cErrorSpec\u003e,\n+        current_signal: \u0026mut Option\u003cSignalSpec\u003e,\n+        current_capability: \u0026mut Option\u003cCapabilitySpec\u003e,\n+        current_side_effect: \u0026mut Option\u003cSideEffectSpec\u003e,\n+        current_constraint: \u0026mut Option\u003cConstraintSpec\u003e,\n+        current_lock: \u0026mut Option\u003cLockSpec\u003e,\n+        current_return: \u0026mut Option\u003cReturnSpec\u003e,\n+        current_state_trans: \u0026mut Option\u003cStateTransitionSpec\u003e,\n+    ) {\n+        match block {\n+            BlockContext::Error(_) =\u003e {\n+                if let Some(error) = current_error.take() {\n+                    spec.errors.push(error);\n+                }\n+            }\n+            BlockContext::Signal =\u003e {\n+                if let Some(signal) = current_signal.take() {\n+                    spec.signals.push(signal);\n+                }\n+            }\n+            BlockContext::Capability =\u003e {\n+                if let Some(cap) = current_capability.take() {\n+                    spec.capabilities.push(cap);\n+                }\n+            }\n+            BlockContext::SideEffect =\u003e {\n+                if let Some(effect) = current_side_effect.take() {\n+                    spec.side_effects.push(effect);\n+                }\n+            }\n+            BlockContext::Constraint =\u003e {\n+                if let Some(constraint) = current_constraint.take() {\n+                    spec.constraints.push(constraint);\n+                }\n+            }\n+            BlockContext::Lock =\u003e {\n+                if let Some(lock) = current_lock.take() {\n+                    spec.locks.push(lock);\n+                }\n+            }\n+            BlockContext::Return =\u003e {\n+                if let Some(mut ret) = current_return.take() {\n+                    ret.keep_used_success_fields();\n+                    spec.return_spec = Some(ret);\n+                }\n+            }\n+            BlockContext::StateTransition =\u003e {\n+                if let Some(trans) = current_state_trans.take() {\n+                    spec.state_transitions.push(trans);\n+                }\n+            }\n+            _ =\u003e {}\n+        }\n+        *block = BlockContext::None;\n+    }\n+\n+    /// Extract parameter type names from SYSCALL_DEFINE signature\n+    fn extract_types_from_signature(\u0026self, sig: \u0026str) -\u003e HashMap\u003cString, String\u003e {\n+        let mut types = HashMap::new();\n+\n+        // Find content between outermost parens\n+        let content = if let Some(start) = sig.find('(') {\n+            let end = sig.rfind(')').unwrap_or(sig.len());\n+            \u0026sig[start + 1..end]\n+        } else {\n+            return types;\n+        };\n+\n+        // Split by comma and process type/name pairs\n+        // SYSCALL_DEFINE format: (syscall_name, type1, name1, type2, name2, ...)\n+        let parts: Vec\u003c\u0026str\u003e = content.split(',').map(|s| s.trim()).collect();\n+\n+        // Skip first part (syscall name), then process pairs\n+        let mut i = 1;\n+        while i + 1 \u003c parts.len() {\n+            let type_part = parts[i].trim();\n+            let name_part = parts[i + 1].trim();\n+\n+            // Build the type_name string: \"type name\"\n+            let type_name = format!(\"{} {}\", type_part, name_part);\n+            types.insert(name_part.to_string(), type_name);\n+\n+            i += 2;\n+        }\n+\n+        types\n+    }\n+\n+    /// Collect the raw lines of a free-form section: the text after the\n+    /// key plus every following indented or blank line, up to the next\n+    /// annotation line.\n+    fn collect_section\u003c'a\u003e(\n+        \u0026self,\n+        lines: \u0026[\u0026'a str],\n+        start_idx: usize,\n+        first_part: \u0026'a str,\n+    ) -\u003e (Vec\u003c\u0026'a str\u003e, usize) {\n+        let mut section = vec![first_part];\n+        let mut i = start_idx + 1;\n+\n+        while i \u003c lines.len() {\n+            let line = lines[i];\n+            if self.is_annotation_line(line) {\n+                break;\n+            }\n+            if !line.trim().is_empty() \u0026\u0026 !is_indented_line(line) {\n+                break;\n+            }\n+            section.push(line);\n+            i += 1;\n+        }\n+\n+        (section, i)\n+    }\n+\n+    fn is_annotation_line(\u0026self, line: \u0026str) -\u003e bool {\n+        let trimmed = line.trim_start();\n+        if !trimmed.contains(':') {\n+            return false;\n+        }\n+        let annotations = [\n+            \"param:\",\n+            \"param-count:\",\n+            \"error:\",\n+            \"lock:\",\n+            \"signal:\",\n+            \"side-effect:\",\n+            \"state-trans:\",\n+            \"capability:\",\n+            \"constraint:\",\n+            \"return:\",\n+            \"examples:\",\n+            \"notes:\",\n+            \"context-\",\n+            \"long-desc:\",\n+            \"api-type:\",\n+        ];\n+\n+        for ann in \u0026annotations {\n+            if trimmed.starts_with(ann) {\n+                return true;\n+            }\n+        }\n+        false\n+    }\n+\n+    /// Parse a constraint expression and apply it to `param`.\n+    /// Shapes:\n+    ///   NAME                         (e.g. \"user_path\", \"nonzero\")\n+    ///   NAME ( ARG (, ARG)* )        (e.g. \"range(0, 4096)\", \"buffer(2)\")\n+    /// Returns true if the expression matched a known constraint kind,\n+    /// populating `param`'s numeric fields. Returns false if the text\n+    /// is free-form, leaving `param` untouched.\n+    fn apply_constraint_expr(\u0026self, param: \u0026mut ParamSpec, text: \u0026str) -\u003e bool {\n+        let t = text.trim();\n+        if t.is_empty() {\n+            return false;\n+        }\n+        // Split NAME ( ARGS ); no nesting, no escaping.\n+        let (name, args): (\u0026str, Option\u003c\u0026str\u003e) = match (t.find('('), t.rfind(')')) {\n+            (Some(lp), Some(rp)) if rp \u003e lp =\u003e (t[..lp].trim(), Some(t[lp + 1..rp].trim())),\n+            _ =\u003e (t, None),\n+        };\n+        // Bail out on anything that looks like free text (spaces inside the\n+        // name part).\n+        if name.contains(char::is_whitespace) || name.is_empty() {\n+            return false;\n+        }\n+        let name_lc = name.to_ascii_lowercase();\n+        let split_args = || -\u003e Vec\u003cString\u003e {\n+            args.map(|a| a.split(',').map(|s| s.trim().to_string()).collect())\n+                .unwrap_or_default()\n+        };\n+        match name_lc.as_str() {\n+            \"range\" =\u003e {\n+                let a = split_args();\n+                if a.len() != 2 {\n+                    return false;\n+                }\n+                param.min_value = parse_i64_literal(\u0026a[0]);\n+                param.max_value = parse_i64_literal(\u0026a[1]);\n+                param.constraint_type = 1; // KAPI_CONSTRAINT_RANGE\n+                true\n+            }\n+            \"mask\" =\u003e {\n+                let a = split_args();\n+                if a.len() != 1 {\n+                    return false;\n+                }\n+                // Symbolic masks (e.g. \"O_RDONLY | O_WRONLY | ...\") can't\n+                // be resolved at parse time; leave valid_mask as None so\n+                // downstream consumers treat the mask as unknown, matching\n+                // the long-form `valid-mask:` handler (which also leaves\n+                // the slot untouched when the value isn't a literal).\n+                param.valid_mask = parse_u64_literal(\u0026a[0]);\n+                param.constraint_type = 2; // KAPI_CONSTRAINT_MASK\n+                true\n+            }\n+            \"enum\" =\u003e {\n+                let a = split_args();\n+                if a.is_empty() {\n+                    return false;\n+                }\n+                // --vmlinux and --debugfs report each value in decimal, so\n+                // numeric literals are normalised; symbolic names stay.\n+                param.enum_values = a\n+                    .into_iter()\n+                    .map(|v| parse_i64_literal(\u0026v).map_or(v, |n| n.to_string()))\n+                    .collect();\n+                param.constraint_type = 3; // KAPI_CONSTRAINT_ENUM\n+                true\n+            }\n+            \"alignment\" | \"align\" =\u003e {\n+                let a = split_args();\n+                if a.len() != 1 {\n+                    return false;\n+                }\n+                param.alignment = parse_u64_literal(\u0026a[0]).and_then(|n| u32::try_from(n).ok());\n+                param.constraint_type = 4; // KAPI_CONSTRAINT_ALIGNMENT\n+                true\n+            }\n+            \"power_of_two\" =\u003e {\n+                if args.is_some() {\n+                    return false;\n+                }\n+                param.constraint_type = 5; // KAPI_CONSTRAINT_POWER_OF_TWO\n+                true\n+            }\n+            \"page_aligned\" =\u003e {\n+                if args.is_some() {\n+                    return false;\n+                }\n+                param.constraint_type = 6; // KAPI_CONSTRAINT_PAGE_ALIGNED\n+                true\n+            }\n+            \"nonzero\" =\u003e {\n+                if args.is_some() {\n+                    return false;\n+                }\n+                param.constraint_type = 7; // KAPI_CONSTRAINT_NONZERO\n+                true\n+            }\n+            \"user_string\" =\u003e {\n+                // Optional size argument: user_string(N)\n+                if let Some(arg) = args {\n+                    if let Some(n) = parse_u64_literal(arg).and_then(|n| u32::try_from(n).ok()) {\n+                        param.size = Some(n);\n+                    }\n+                }\n+                param.constraint_type = 8; // KAPI_CONSTRAINT_USER_STRING\n+                true\n+            }\n+            \"user_path\" =\u003e {\n+                if args.is_some() {\n+                    return false;\n+                }\n+                param.constraint_type = 9; // KAPI_CONSTRAINT_USER_PATH\n+                true\n+            }\n+            \"user_ptr\" =\u003e {\n+                if args.is_some() {\n+                    return false;\n+                }\n+                param.constraint_type = 10; // KAPI_CONSTRAINT_USER_PTR\n+                true\n+            }\n+            \"buffer\" =\u003e {\n+                // buffer(size_param_idx): capture the index into\n+                // param.size_param_idx so it matches the long-form\n+                // `size-param: N` handler below (and the C struct\n+                // field populated by KAPI_PARAM_SIZE_PARAM()).\n+                let a = split_args();\n+                if a.len() != 1 {\n+                    return false;\n+                }\n+                param.size_param_idx = a[0].parse().ok();\n+                param.constraint_type = 11; // KAPI_CONSTRAINT_BUFFER\n+                true\n+            }\n+            \"custom\" =\u003e {\n+                // custom(fn_name): record function name as free-text constraint\n+                // so downstream tooling can wire it up.\n+                if let Some(arg) = args {\n+                    param.constraint = Some(arg.trim().to_string());\n+                }\n+                param.constraint_type = 12; // KAPI_CONSTRAINT_CUSTOM\n+                true\n+            }\n+            _ =\u003e false,\n+        }\n+    }\n+\n+    fn parse_context_flags(\u0026self, flags: \u0026str) -\u003e Vec\u003cString\u003e {\n+        flags\n+            .split('|')\n+            .map(|f| self.ctx_alias(f.trim()).to_string())\n+            .filter(|f| !f.is_empty())\n+            .collect()\n+    }\n+\n+    /// Parse a comma-separated short-form context list\n+    /// (e.g. \"process, sleepable\" -\u003e [\"KAPI_CTX_PROCESS\", \"KAPI_CTX_SLEEPABLE\"]).\n+    /// Tokens that already look like KAPI_CTX_* are passed through.\n+    fn parse_context_list(\u0026self, flags: \u0026str) -\u003e Vec\u003cString\u003e {\n+        flags\n+            .split(',')\n+            .map(|f| self.ctx_alias(f.trim()).to_string())\n+            .filter(|f| !f.is_empty())\n+            .collect()\n+    }\n+\n+    /// Canonicalise a single context token to its KAPI_CTX_* spelling.\n+    /// Short aliases are case-insensitive. Unknown tokens pass through\n+    /// verbatim so mixed/long-form input keeps working.\n+    fn ctx_alias(\u0026self, tok: \u0026str) -\u003e String {\n+        let t = tok.trim();\n+        if t.is_empty() {\n+            return String::new();\n+        }\n+        match t.to_ascii_lowercase().as_str() {\n+            \"process\" =\u003e \"KAPI_CTX_PROCESS\".to_string(),\n+            \"softirq\" =\u003e \"KAPI_CTX_SOFTIRQ\".to_string(),\n+            \"hardirq\" =\u003e \"KAPI_CTX_HARDIRQ\".to_string(),\n+            \"nmi\" =\u003e \"KAPI_CTX_NMI\".to_string(),\n+            \"atomic\" =\u003e \"KAPI_CTX_ATOMIC\".to_string(),\n+            \"sleepable\" =\u003e \"KAPI_CTX_SLEEPABLE\".to_string(),\n+            \"preempt_disabled\" =\u003e \"KAPI_CTX_PREEMPT_DISABLED\".to_string(),\n+            \"irq_disabled\" =\u003e \"KAPI_CTX_IRQ_DISABLED\".to_string(),\n+            _ =\u003e t.to_string(),\n+        }\n+    }\n+\n+    fn error_name_to_code(\u0026self, name: \u0026str) -\u003e i32 {\n+        match name {\n+            \"EPERM\" =\u003e -1,\n+            \"ENOENT\" =\u003e -2,\n+            \"ESRCH\" =\u003e -3,\n+            \"EINTR\" =\u003e -4,\n+            \"EIO\" =\u003e -5,\n+            \"ENXIO\" =\u003e -6,\n+            \"E2BIG\" =\u003e -7,\n+            \"ENOEXEC\" =\u003e -8,\n+            \"EBADF\" =\u003e -9,\n+            \"ECHILD\" =\u003e -10,\n+            \"EAGAIN\" | \"EWOULDBLOCK\" =\u003e -11,\n+            \"ENOMEM\" =\u003e -12,\n+            \"EACCES\" =\u003e -13,\n+            \"EFAULT\" =\u003e -14,\n+            \"ENOTBLK\" =\u003e -15,\n+            \"EBUSY\" =\u003e -16,\n+            \"EEXIST\" =\u003e -17,\n+            \"EXDEV\" =\u003e -18,\n+            \"ENODEV\" =\u003e -19,\n+            \"ENOTDIR\" =\u003e -20,\n+            \"EISDIR\" =\u003e -21,\n+            \"EINVAL\" =\u003e -22,\n+            \"ENFILE\" =\u003e -23,\n+            \"EMFILE\" =\u003e -24,\n+            \"ENOTTY\" =\u003e -25,\n+            \"ETXTBSY\" =\u003e -26,\n+            \"EFBIG\" =\u003e -27,\n+            \"ENOSPC\" =\u003e -28,\n+            \"ESPIPE\" =\u003e -29,\n+            \"EROFS\" =\u003e -30,\n+            \"EMLINK\" =\u003e -31,\n+            \"EPIPE\" =\u003e -32,\n+            \"EDOM\" =\u003e -33,\n+            \"ERANGE\" =\u003e -34,\n+            \"EDEADLK\" =\u003e -35,\n+            \"ENAMETOOLONG\" =\u003e -36,\n+            \"ENOLCK\" =\u003e -37,\n+            \"ENOSYS\" =\u003e -38,\n+            \"ENOTEMPTY\" =\u003e -39,\n+            \"ELOOP\" =\u003e -40,\n+            \"ENOMSG\" =\u003e -42,\n+            \"ENODATA\" =\u003e -61,\n+            \"ENOLINK\" =\u003e -67,\n+            \"EPROTO\" =\u003e -71,\n+            \"EOVERFLOW\" =\u003e -75,\n+            \"ELIBBAD\" =\u003e -80,\n+            \"EILSEQ\" =\u003e -84,\n+            \"ENOTSOCK\" =\u003e -88,\n+            \"EDESTADDRREQ\" =\u003e -89,\n+            \"EMSGSIZE\" =\u003e -90,\n+            \"EPROTOTYPE\" =\u003e -91,\n+            \"ENOPROTOOPT\" =\u003e -92,\n+            \"EPROTONOSUPPORT\" =\u003e -93,\n+            \"EOPNOTSUPP\" | \"ENOTSUP\" =\u003e -95,\n+            \"EADDRINUSE\" =\u003e -98,\n+            \"EADDRNOTAVAIL\" =\u003e -99,\n+            \"ENETDOWN\" =\u003e -100,\n+            \"ENETUNREACH\" =\u003e -101,\n+            \"ENETRESET\" =\u003e -102,\n+            \"ECONNABORTED\" =\u003e -103,\n+            \"ECONNRESET\" =\u003e -104,\n+            \"ENOBUFS\" =\u003e -105,\n+            \"EISCONN\" =\u003e -106,\n+            \"ENOTCONN\" =\u003e -107,\n+            \"ETIMEDOUT\" =\u003e -110,\n+            \"ECONNREFUSED\" =\u003e -111,\n+            \"EALREADY\" =\u003e -114,\n+            \"EINPROGRESS\" =\u003e -115,\n+            \"ESTALE\" =\u003e -116,\n+            \"EDQUOT\" =\u003e -122,\n+            \"ENOMEDIUM\" =\u003e -123,\n+            \"ENOKEY\" =\u003e -126,\n+            \"EHWPOISON\" =\u003e -133,\n+            \"ERESTARTSYS\" =\u003e -512,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Map a KAPI_TYPE_* token (or its short-form alias) to the numeric\n+    /// value declared in `enum kapi_param_type` in\n+    /// `include/linux/kernel_api_spec.h`.\n+    fn parse_param_type(\u0026self, type_str: \u0026str) -\u003e u32 {\n+        let s = type_str.trim();\n+        match s {\n+            \"KAPI_TYPE_VOID\" =\u003e 0,\n+            \"KAPI_TYPE_INT\" =\u003e 1,\n+            \"KAPI_TYPE_UINT\" =\u003e 2,\n+            \"KAPI_TYPE_PTR\" =\u003e 3,\n+            \"KAPI_TYPE_STRUCT\" =\u003e 4,\n+            \"KAPI_TYPE_UNION\" =\u003e 5,\n+            \"KAPI_TYPE_ENUM\" =\u003e 6,\n+            \"KAPI_TYPE_FUNC_PTR\" =\u003e 7,\n+            \"KAPI_TYPE_ARRAY\" =\u003e 8,\n+            \"KAPI_TYPE_FD\" =\u003e 9,\n+            \"KAPI_TYPE_USER_PTR\" =\u003e 10,\n+            \"KAPI_TYPE_PATH\" =\u003e 11,\n+            \"KAPI_TYPE_CUSTOM\" =\u003e 12,\n+            _ =\u003e match s.to_ascii_lowercase().as_str() {\n+                \"void\" =\u003e 0,\n+                \"int\" =\u003e 1,\n+                \"uint\" =\u003e 2,\n+                \"ptr\" =\u003e 3,\n+                \"struct\" =\u003e 4,\n+                \"union\" =\u003e 5,\n+                \"enum\" =\u003e 6,\n+                \"func_ptr\" =\u003e 7,\n+                \"array\" =\u003e 8,\n+                \"fd\" =\u003e 9,\n+                \"user_ptr\" | \"uptr\" =\u003e 10,\n+                \"path\" =\u003e 11,\n+                \"custom\" =\u003e 12,\n+                _ =\u003e 0,\n+            },\n+        }\n+    }\n+\n+    /// Map a KAPI_CONSTRAINT_* token to the numeric value declared in\n+    /// `enum kapi_constraint_type` in `include/linux/kernel_api_spec.h`.\n+    fn parse_constraint_type(\u0026self, type_str: \u0026str) -\u003e u32 {\n+        let s = type_str.trim();\n+        match s {\n+            \"KAPI_CONSTRAINT_NONE\" =\u003e 0,\n+            \"KAPI_CONSTRAINT_RANGE\" =\u003e 1,\n+            \"KAPI_CONSTRAINT_MASK\" =\u003e 2,\n+            \"KAPI_CONSTRAINT_ENUM\" =\u003e 3,\n+            \"KAPI_CONSTRAINT_ALIGNMENT\" =\u003e 4,\n+            \"KAPI_CONSTRAINT_POWER_OF_TWO\" =\u003e 5,\n+            \"KAPI_CONSTRAINT_PAGE_ALIGNED\" =\u003e 6,\n+            \"KAPI_CONSTRAINT_NONZERO\" =\u003e 7,\n+            \"KAPI_CONSTRAINT_USER_STRING\" =\u003e 8,\n+            \"KAPI_CONSTRAINT_USER_PATH\" =\u003e 9,\n+            \"KAPI_CONSTRAINT_USER_PTR\" =\u003e 10,\n+            \"KAPI_CONSTRAINT_BUFFER\" =\u003e 11,\n+            \"KAPI_CONSTRAINT_CUSTOM\" =\u003e 12,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    fn parse_param_flags(\u0026self, flags: \u0026str) -\u003e u32 {\n+        flags\n+            .split('|')\n+            .map(|f| self.parse_param_flag_token(f.trim()))\n+            .fold(0, |acc, bit| acc | bit)\n+    }\n+\n+    /// Parse one flag token (long or short form, case-insensitive for\n+    /// short form). Returns 0 for unknown tokens.\n+    fn parse_param_flag_token(\u0026self, tok: \u0026str) -\u003e u32 {\n+        let t = tok.trim();\n+        // KAPI_PARAM_* names and upper-case short forms first.\n+        match t {\n+            \"KAPI_PARAM_IN\" | \"IN\" =\u003e return 1,\n+            \"KAPI_PARAM_OUT\" | \"OUT\" =\u003e return 2,\n+            \"KAPI_PARAM_INOUT\" | \"INOUT\" =\u003e return 3,\n+            \"KAPI_PARAM_OPTIONAL\" | \"OPTIONAL\" =\u003e return 1 \u003c\u003c 3,\n+            \"KAPI_PARAM_CONST\" | \"CONST\" =\u003e return 1 \u003c\u003c 4,\n+            \"KAPI_PARAM_VOLATILE\" | \"VOLATILE\" =\u003e return 1 \u003c\u003c 5,\n+            \"KAPI_PARAM_USER\" | \"USER\" =\u003e return 1 \u003c\u003c 6,\n+            \"KAPI_PARAM_DMA\" | \"DMA\" =\u003e return 1 \u003c\u003c 7,\n+            \"KAPI_PARAM_ALIGNED\" | \"ALIGNED\" =\u003e return 1 \u003c\u003c 8,\n+            _ =\u003e {}\n+        }\n+        // English short aliases (case-insensitive).\n+        match t.to_ascii_lowercase().as_str() {\n+            \"input\" =\u003e 1,\n+            \"output\" =\u003e 2,\n+            \"inout\" =\u003e 3,\n+            \"optional\" =\u003e 1 \u003c\u003c 3,\n+            \"const\" =\u003e 1 \u003c\u003c 4,\n+            \"volatile\" =\u003e 1 \u003c\u003c 5,\n+            \"user\" =\u003e 1 \u003c\u003c 6,\n+            \"dma\" =\u003e 1 \u003c\u003c 7,\n+            \"aligned\" =\u003e 1 \u003c\u003c 8,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Map a KAPI_LOCK_* token to the numeric value declared in\n+    /// `enum kapi_lock_type` in `include/linux/kernel_api_spec.h`.\n+    fn parse_lock_type(\u0026self, type_str: \u0026str) -\u003e u32 {\n+        let s = type_str.trim();\n+        match s {\n+            \"KAPI_LOCK_NONE\" =\u003e 0,\n+            \"KAPI_LOCK_MUTEX\" =\u003e 1,\n+            \"KAPI_LOCK_SPINLOCK\" =\u003e 2,\n+            \"KAPI_LOCK_RWLOCK\" =\u003e 3,\n+            \"KAPI_LOCK_SEQLOCK\" =\u003e 4,\n+            \"KAPI_LOCK_RCU\" =\u003e 5,\n+            \"KAPI_LOCK_SEMAPHORE\" =\u003e 6,\n+            \"KAPI_LOCK_CUSTOM\" =\u003e 7,\n+            _ =\u003e match s.to_ascii_lowercase().as_str() {\n+                \"none\" =\u003e 0,\n+                \"mutex\" =\u003e 1,\n+                \"spinlock\" =\u003e 2,\n+                \"rwlock\" =\u003e 3,\n+                \"seqlock\" =\u003e 4,\n+                \"rcu\" =\u003e 5,\n+                \"semaphore\" =\u003e 6,\n+                \"custom\" =\u003e 7,\n+                _ =\u003e 0,\n+            },\n+        }\n+    }\n+\n+    fn parse_signal_direction(\u0026self, dir: \u0026str) -\u003e u32 {\n+        let s = dir.trim();\n+        match s {\n+            \"KAPI_SIGNAL_RECEIVE\" =\u003e 1,\n+            \"KAPI_SIGNAL_SEND\" =\u003e 2,\n+            \"KAPI_SIGNAL_HANDLE\" =\u003e 4,\n+            \"KAPI_SIGNAL_BLOCK\" =\u003e 8,\n+            \"KAPI_SIGNAL_IGNORE\" =\u003e 16,\n+            _ =\u003e match s.to_ascii_lowercase().as_str() {\n+                \"receive\" =\u003e 1,\n+                \"send\" =\u003e 2,\n+                \"handle\" =\u003e 4,\n+                \"block\" =\u003e 8,\n+                \"ignore\" =\u003e 16,\n+                _ =\u003e 0,\n+            },\n+        }\n+    }\n+\n+    fn parse_signal_action(\u0026self, action: \u0026str) -\u003e u32 {\n+        let s = action.trim();\n+        match s {\n+            \"KAPI_SIGNAL_ACTION_DEFAULT\" =\u003e 0,\n+            \"KAPI_SIGNAL_ACTION_TERMINATE\" =\u003e 1,\n+            \"KAPI_SIGNAL_ACTION_COREDUMP\" =\u003e 2,\n+            \"KAPI_SIGNAL_ACTION_STOP\" =\u003e 3,\n+            \"KAPI_SIGNAL_ACTION_CONTINUE\" =\u003e 4,\n+            \"KAPI_SIGNAL_ACTION_CUSTOM\" =\u003e 5,\n+            \"KAPI_SIGNAL_ACTION_RETURN\" =\u003e 6,\n+            \"KAPI_SIGNAL_ACTION_RESTART\" =\u003e 7,\n+            \"KAPI_SIGNAL_ACTION_QUEUE\" =\u003e 8,\n+            \"KAPI_SIGNAL_ACTION_DISCARD\" =\u003e 9,\n+            \"KAPI_SIGNAL_ACTION_TRANSFORM\" =\u003e 10,\n+            _ =\u003e match s.to_ascii_lowercase().as_str() {\n+                \"default\" =\u003e 0,\n+                \"terminate\" =\u003e 1,\n+                \"coredump\" =\u003e 2,\n+                \"stop\" =\u003e 3,\n+                \"continue\" =\u003e 4,\n+                \"custom\" =\u003e 5,\n+                \"return\" =\u003e 6,\n+                \"restart\" =\u003e 7,\n+                \"queue\" =\u003e 8,\n+                \"discard\" =\u003e 9,\n+                \"transform\" =\u003e 10,\n+                _ =\u003e 0,\n+            },\n+        }\n+    }\n+\n+    fn parse_signal_timing(\u0026self, timing: \u0026str) -\u003e u32 {\n+        let s = timing.trim();\n+        match s {\n+            \"KAPI_SIGNAL_TIME_BEFORE\" =\u003e 0,\n+            \"KAPI_SIGNAL_TIME_DURING\" =\u003e 1,\n+            \"KAPI_SIGNAL_TIME_AFTER\" =\u003e 2,\n+            _ =\u003e match s.to_ascii_lowercase().as_str() {\n+                \"before\" =\u003e 0,\n+                \"during\" =\u003e 1,\n+                \"after\" =\u003e 2,\n+                _ =\u003e 0,\n+            },\n+        }\n+    }\n+\n+    /// Accept a hex literal (\"0x4\"), a decimal literal (\"4\"), or a '|'-separated\n+    /// bitmask expression. Unknown tokens contribute 0.\n+    fn parse_hex_or_bitmask(\u0026self, value: \u0026str) -\u003e u32 {\n+        let v = value.trim();\n+        if let Some(hex) = v.strip_prefix(\"0x\").or_else(|| v.strip_prefix(\"0X\")) {\n+            if let Ok(n) = u32::from_str_radix(hex, 16) {\n+                return n;\n+            }\n+        }\n+        if let Ok(n) = v.parse::\u003cu32\u003e() {\n+            return n;\n+        }\n+        let mut acc = 0u32;\n+        for part in v.split(['|', ',']) {\n+            let t = part.trim();\n+            if t.is_empty() {\n+                continue;\n+            }\n+            if let Some(hex) = t.strip_prefix(\"0x\").or_else(|| t.strip_prefix(\"0X\")) {\n+                if let Ok(n) = u32::from_str_radix(hex, 16) {\n+                    acc |= n;\n+                    continue;\n+                }\n+            }\n+            if let Ok(n) = t.parse::\u003cu32\u003e() {\n+                acc |= n;\n+            }\n+        }\n+        acc\n+    }\n+\n+    /// Parse a '|'-separated list of KAPI_SIGNAL_STATE_* tokens (or short\n+    /// names like \"RUNNING\") and OR their bit values together. Matches the\n+    /// BIT(N) definitions in kernel_api_spec.h.\n+    fn parse_signal_state_mask(\u0026self, value: \u0026str) -\u003e u32 {\n+        let mut acc = 0u32;\n+        for part in value.split(['|', ',']) {\n+            let t = part.trim().trim_start_matches(\"KAPI_SIGNAL_STATE_\");\n+            let bit = match t.to_ascii_uppercase().as_str() {\n+                \"RUNNING\" =\u003e 1 \u003c\u003c 0,\n+                \"SLEEPING\" =\u003e 1 \u003c\u003c 1,\n+                \"STOPPED\" =\u003e 1 \u003c\u003c 2,\n+                \"TRACED\" =\u003e 1 \u003c\u003c 3,\n+                \"ZOMBIE\" =\u003e 1 \u003c\u003c 4,\n+                \"DEAD\" =\u003e 1 \u003c\u003c 5,\n+                _ =\u003e 0,\n+            };\n+            acc |= bit;\n+        }\n+        acc\n+    }\n+\n+    /// Bitmask of `KAPI_EFFECT_*` values joined by '|' or ','.\n+    /// Values match `enum kapi_side_effect_type` in\n+    /// `include/linux/kernel_api_spec.h`.\n+    fn parse_effect_type(\u0026self, type_str: \u0026str) -\u003e u32 {\n+        let sep = if type_str.contains('|') || !type_str.contains(',') {\n+            '|'\n+        } else {\n+            ','\n+        };\n+        let mut result = 0;\n+        for flag in type_str.split(sep) {\n+            let t = flag.trim();\n+            let bit = match t {\n+                \"KAPI_EFFECT_NONE\" =\u003e 0,\n+                \"KAPI_EFFECT_ALLOC_MEMORY\" =\u003e 1 \u003c\u003c 0,\n+                \"KAPI_EFFECT_FREE_MEMORY\" =\u003e 1 \u003c\u003c 1,\n+                \"KAPI_EFFECT_MODIFY_STATE\" =\u003e 1 \u003c\u003c 2,\n+                \"KAPI_EFFECT_SIGNAL_SEND\" =\u003e 1 \u003c\u003c 3,\n+                \"KAPI_EFFECT_FILE_POSITION\" =\u003e 1 \u003c\u003c 4,\n+                \"KAPI_EFFECT_LOCK_ACQUIRE\" =\u003e 1 \u003c\u003c 5,\n+                \"KAPI_EFFECT_LOCK_RELEASE\" =\u003e 1 \u003c\u003c 6,\n+                \"KAPI_EFFECT_RESOURCE_CREATE\" =\u003e 1 \u003c\u003c 7,\n+                \"KAPI_EFFECT_RESOURCE_DESTROY\" =\u003e 1 \u003c\u003c 8,\n+                \"KAPI_EFFECT_SCHEDULE\" =\u003e 1 \u003c\u003c 9,\n+                \"KAPI_EFFECT_HARDWARE\" =\u003e 1 \u003c\u003c 10,\n+                \"KAPI_EFFECT_NETWORK\" =\u003e 1 \u003c\u003c 11,\n+                \"KAPI_EFFECT_FILESYSTEM\" =\u003e 1 \u003c\u003c 12,\n+                \"KAPI_EFFECT_PROCESS_STATE\" =\u003e 1 \u003c\u003c 13,\n+                \"KAPI_EFFECT_IRREVERSIBLE\" =\u003e 1 \u003c\u003c 14,\n+                _ =\u003e match t.to_ascii_lowercase().as_str() {\n+                    \"none\" =\u003e 0,\n+                    \"alloc_memory\" =\u003e 1 \u003c\u003c 0,\n+                    \"free_memory\" =\u003e 1 \u003c\u003c 1,\n+                    \"modify_state\" =\u003e 1 \u003c\u003c 2,\n+                    \"signal_send\" =\u003e 1 \u003c\u003c 3,\n+                    \"file_position\" =\u003e 1 \u003c\u003c 4,\n+                    \"lock_acquire\" =\u003e 1 \u003c\u003c 5,\n+                    \"lock_release\" =\u003e 1 \u003c\u003c 6,\n+                    \"resource_create\" =\u003e 1 \u003c\u003c 7,\n+                    \"resource_destroy\" =\u003e 1 \u003c\u003c 8,\n+                    \"schedule\" =\u003e 1 \u003c\u003c 9,\n+                    \"hardware\" =\u003e 1 \u003c\u003c 10,\n+                    \"network\" =\u003e 1 \u003c\u003c 11,\n+                    \"filesystem\" =\u003e 1 \u003c\u003c 12,\n+                    \"process_state\" =\u003e 1 \u003c\u003c 13,\n+                    \"irreversible\" =\u003e 1 \u003c\u003c 14,\n+                    _ =\u003e 0,\n+                },\n+            };\n+            result |= bit;\n+        }\n+        result\n+    }\n+\n+    fn parse_capability_value(\u0026self, cap: \u0026str) -\u003e i32 {\n+        match cap {\n+            \"CAP_CHOWN\" =\u003e 0,\n+            \"CAP_DAC_OVERRIDE\" =\u003e 1,\n+            \"CAP_DAC_READ_SEARCH\" =\u003e 2,\n+            \"CAP_FOWNER\" =\u003e 3,\n+            \"CAP_FSETID\" =\u003e 4,\n+            \"CAP_KILL\" =\u003e 5,\n+            \"CAP_SETGID\" =\u003e 6,\n+            \"CAP_SETUID\" =\u003e 7,\n+            \"CAP_SETPCAP\" =\u003e 8,\n+            \"CAP_LINUX_IMMUTABLE\" =\u003e 9,\n+            \"CAP_NET_BIND_SERVICE\" =\u003e 10,\n+            \"CAP_NET_BROADCAST\" =\u003e 11,\n+            \"CAP_NET_ADMIN\" =\u003e 12,\n+            \"CAP_NET_RAW\" =\u003e 13,\n+            \"CAP_IPC_LOCK\" =\u003e 14,\n+            \"CAP_IPC_OWNER\" =\u003e 15,\n+            \"CAP_SYS_MODULE\" =\u003e 16,\n+            \"CAP_SYS_RAWIO\" =\u003e 17,\n+            \"CAP_SYS_CHROOT\" =\u003e 18,\n+            \"CAP_SYS_PTRACE\" =\u003e 19,\n+            \"CAP_SYS_PACCT\" =\u003e 20,\n+            \"CAP_SYS_ADMIN\" =\u003e 21,\n+            \"CAP_SYS_BOOT\" =\u003e 22,\n+            \"CAP_SYS_NICE\" =\u003e 23,\n+            \"CAP_SYS_RESOURCE\" =\u003e 24,\n+            \"CAP_SYS_TIME\" =\u003e 25,\n+            \"CAP_SYS_TTY_CONFIG\" =\u003e 26,\n+            \"CAP_MKNOD\" =\u003e 27,\n+            \"CAP_LEASE\" =\u003e 28,\n+            \"CAP_AUDIT_WRITE\" =\u003e 29,\n+            \"CAP_AUDIT_CONTROL\" =\u003e 30,\n+            \"CAP_SETFCAP\" =\u003e 31,\n+            \"CAP_MAC_OVERRIDE\" =\u003e 32,\n+            \"CAP_MAC_ADMIN\" =\u003e 33,\n+            \"CAP_SYSLOG\" =\u003e 34,\n+            \"CAP_WAKE_ALARM\" =\u003e 35,\n+            \"CAP_BLOCK_SUSPEND\" =\u003e 36,\n+            \"CAP_AUDIT_READ\" =\u003e 37,\n+            \"CAP_PERFMON\" =\u003e 38,\n+            \"CAP_BPF\" =\u003e 39,\n+            \"CAP_CHECKPOINT_RESTORE\" =\u003e 40,\n+            _ =\u003e 0,\n+        }\n+    }\n+\n+    /// Map a KAPI_RETURN_* token to the numeric value declared in\n+    /// `enum kapi_return_check_type` in `include/linux/kernel_api_spec.h`.\n+    fn parse_return_check_type(\u0026self, check: \u0026str) -\u003e u32 {\n+        let s = check.trim();\n+        match s {\n+            \"KAPI_RETURN_EXACT\" =\u003e 0,\n+            \"KAPI_RETURN_RANGE\" =\u003e 1,\n+            \"KAPI_RETURN_ERROR_CHECK\" =\u003e 2,\n+            \"KAPI_RETURN_FD\" =\u003e 3,\n+            \"KAPI_RETURN_CUSTOM\" =\u003e 4,\n+            \"KAPI_RETURN_NO_RETURN\" =\u003e 5,\n+            _ =\u003e match s.to_ascii_lowercase().as_str() {\n+                \"exact\" =\u003e 0,\n+                \"range\" =\u003e 1,\n+                \"error_check\" =\u003e 2,\n+                \"fd\" =\u003e 3,\n+                \"custom\" =\u003e 4,\n+                \"no_return\" =\u003e 5,\n+                _ =\u003e 0,\n+            },\n+        }\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+\n+    fn parser() -\u003e KerneldocParser {\n+        KerneldocParser::new()\n+    }\n+\n+    #[test]\n+    fn parse_minimal_kerneldoc() {\n+        let doc = \"\\\n+sys_foo - Do something useful\n+context-flags: KAPI_CTX_PROCESS\n+param-count: 1\n+@fd: The file descriptor\n+error: EBADF, Bad file descriptor\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_foo\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.name, \"sys_foo\");\n+        assert_eq!(spec.api_type, \"syscall\");\n+        assert_eq!(spec.description.as_deref(), Some(\"Do something useful\"));\n+        assert_eq!(spec.param_count, Some(1));\n+        assert_eq!(spec.parameters.len(), 1);\n+        assert_eq!(spec.parameters[0].name, \"fd\");\n+        assert_eq!(spec.parameters[0].description, \"The file descriptor\");\n+        assert_eq!(spec.errors.len(), 1);\n+        assert_eq!(spec.errors[0].name, \"EBADF\");\n+        assert_eq!(spec.errors[0].error_code, -9);\n+    }\n+\n+    #[test]\n+    fn parse_multiple_param_types() {\n+        let doc = \"\\\n+sys_bar - Multiple params\n+@fd: file descriptor arg\n+@buf: user buffer\n+@count: byte count\n+@flags: option flags\n+param: fd\n+  type: KAPI_TYPE_FD\n+param: buf\n+  type: KAPI_TYPE_USER_PTR\n+param: count\n+  type: KAPI_TYPE_UINT\n+param: flags\n+  type: KAPI_TYPE_UINT\n+\";\n+        let sig = \"(bar, int, fd, char __user *, buf, size_t, count, unsigned long, flags)\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_bar\", \"syscall\", Some(sig))\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters.len(), 4);\n+\n+        let fd_param = spec.parameters.iter().find(|p| p.name == \"fd\").unwrap();\n+        assert_eq!(fd_param.param_type, 9); // FD (kernel enum)\n+\n+        let buf_param = spec.parameters.iter().find(|p| p.name == \"buf\").unwrap();\n+        assert_eq!(buf_param.param_type, 10); // USER_PTR (kernel enum)\n+        assert_eq!(buf_param.type_name, \"char __user * buf\");\n+\n+        let count_param = spec.parameters.iter().find(|p| p.name == \"count\").unwrap();\n+        assert_eq!(count_param.param_type, 2); // UINT\n+\n+        let flags_param = spec.parameters.iter().find(|p| p.name == \"flags\").unwrap();\n+        assert_eq!(flags_param.param_type, 2); // UINT\n+    }\n+\n+    #[test]\n+    fn parse_error_codes_with_descriptions() {\n+        let doc = \"\\\n+sys_err - Error test\n+error: EBADF\n+  desc: Bad file descriptor\n+  condition: fd \u003c 0\n+error: EFAULT\n+  desc: Bad user pointer\n+  condition: buf is NULL\n+error: EINVAL\n+  desc: Invalid argument\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_err\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.errors.len(), 3);\n+\n+        assert_eq!(spec.errors[0].name, \"EBADF\");\n+        assert_eq!(spec.errors[0].error_code, -9);\n+        assert_eq!(spec.errors[0].description, \"Bad file descriptor\");\n+        assert_eq!(spec.errors[0].condition, \"fd \u003c 0\");\n+\n+        assert_eq!(spec.errors[1].name, \"EFAULT\");\n+        assert_eq!(spec.errors[1].error_code, -14);\n+        assert_eq!(spec.errors[1].description, \"Bad user pointer\");\n+\n+        assert_eq!(spec.errors[2].name, \"EINVAL\");\n+        assert_eq!(spec.errors[2].error_code, -22);\n+        assert_eq!(spec.errors[2].description, \"Invalid argument\");\n+    }\n+\n+    #[test]\n+    fn parse_context_flags() {\n+        let doc = \"\\\n+sys_ctx - Context test\n+context-flags: KAPI_CTX_PROCESS|KAPI_CTX_SLEEPABLE\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_ctx\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.context_flags.len(), 2);\n+        assert_eq!(spec.context_flags[0], \"KAPI_CTX_PROCESS\");\n+        assert_eq!(spec.context_flags[1], \"KAPI_CTX_SLEEPABLE\");\n+    }\n+\n+    #[test]\n+    fn parse_context_list_short() {\n+        // \"contexts: process, sleepable\" -\u003e KAPI_CTX_PROCESS | SLEEPABLE\n+        let doc = \"\\\n+sys_ctx - Context test\n+contexts: process, sleepable\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_ctx\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(\n+            spec.context_flags,\n+            vec![\n+                \"KAPI_CTX_PROCESS\".to_string(),\n+                \"KAPI_CTX_SLEEPABLE\".to_string(),\n+            ]\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_context_list_mixed() {\n+        // Short tokens intermixed with explicit KAPI_CTX_* still work.\n+        let doc = \"\\\n+sys_ctx - Context test\n+contexts: process, KAPI_CTX_SLEEPABLE, softirq\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_ctx\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(\n+            spec.context_flags,\n+            vec![\n+                \"KAPI_CTX_PROCESS\".to_string(),\n+                \"KAPI_CTX_SLEEPABLE\".to_string(),\n+                \"KAPI_CTX_SOFTIRQ\".to_string(),\n+            ]\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_context_flags_long_with_short_token() {\n+        // Long-form \"context-flags:\" accepts \"|\"-joined short aliases.\n+        let doc = \"\\\n+sys_ctx - Context test\n+context-flags: process | KAPI_CTX_SLEEPABLE\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_ctx\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(\n+            spec.context_flags,\n+            vec![\n+                \"KAPI_CTX_PROCESS\".to_string(),\n+                \"KAPI_CTX_SLEEPABLE\".to_string(),\n+            ]\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_param_type_short_combined() {\n+        // \"type: uint, input\" combines the type and flag aliases.\n+        let doc = \"\\\n+sys_t - Short type test\n+param: size\n+  type: uint, input\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_t\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters.len(), 1);\n+        assert_eq!(spec.parameters[0].param_type, 2); // KAPI_TYPE_UINT\n+        assert_eq!(spec.parameters[0].flags, 1); // KAPI_PARAM_IN\n+    }\n+\n+    #[test]\n+    fn parse_param_type_short_multi_flag() {\n+        // \"type: path, input, user\" sets both the IN and USER flags.\n+        let doc = \"\\\n+sys_t - Short type test\n+param: filename\n+  type: path, input, user\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_t\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters.len(), 1);\n+        assert_eq!(spec.parameters[0].param_type, 11); // PATH (kernel enum)\n+        assert_eq!(spec.parameters[0].flags, 1 | (1 \u003c\u003c 6)); // IN | USER\n+    }\n+\n+    #[test]\n+    fn parse_constraint_type_range_expr() {\n+        // Short form: \"constraint-type: range(0, 4096)\" replaces the\n+        // two-line long form \"constraint-type: KAPI_CONSTRAINT_RANGE\"\n+        // + \"range: 0, 4096\".\n+        let doc = \"\\\n+sys_c - Constraint test\n+param: count\n+  type: uint, input\n+  constraint-type: range(0, 4096)\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_c\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.constraint_type, 1); // KAPI_CONSTRAINT_RANGE\n+        assert_eq!(p.min_value, Some(0));\n+        assert_eq!(p.max_value, Some(4096));\n+    }\n+\n+    #[test]\n+    fn parse_constraint_type_mask_expr() {\n+        let doc = \"\\\n+sys_c - Constraint test\n+param: flags\n+  type: uint, input\n+  constraint-type: mask(0xff)\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_c\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.constraint_type, 2); // KAPI_CONSTRAINT_MASK\n+        assert_eq!(p.valid_mask, Some(0xff));\n+    }\n+\n+    #[test]\n+    fn parse_constraint_type_enum_expr() {\n+        let doc = \"\\\n+sys_c - Constraint test\n+param: mode\n+  type: int, input\n+  constraint-type: enum(0, 0x10, -3, MODE_FAST)\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_c\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.constraint_type, 3); // KAPI_CONSTRAINT_ENUM\n+        assert_eq!(p.enum_values, [\"0\", \"16\", \"-3\", \"MODE_FAST\"]);\n+    }\n+\n+    #[test]\n+    fn c_integer_literals() {\n+        assert_eq!(parse_u64_literal(\"0\"), Some(0));\n+        assert_eq!(parse_u64_literal(\"010\"), Some(8));\n+        assert_eq!(parse_u64_literal(\"0755\"), Some(0o755));\n+        assert_eq!(parse_u64_literal(\"0x1F\"), Some(31));\n+        assert_eq!(parse_u64_literal(\"0b101\"), Some(5));\n+        assert_eq!(parse_u64_literal(\"4096UL\"), Some(4096));\n+        assert_eq!(parse_u64_literal(\"0xffULL\"), Some(255));\n+        assert_eq!(parse_u64_literal(\"08\"), None);\n+        assert_eq!(parse_u64_literal(\"0x\"), None);\n+        assert_eq!(parse_u64_literal(\"+5\"), None);\n+        assert_eq!(parse_u64_literal(\"PAGE_SIZE\"), None);\n+        assert_eq!(parse_i64_literal(\"-010\"), Some(-8));\n+        assert_eq!(parse_i64_literal(\"-1U\"), Some(4294967295));\n+        assert_eq!(parse_i64_literal(\"-0x80000000\"), Some(2147483648));\n+        assert_eq!(parse_i64_literal(\"-2147483648\"), Some(-2147483648));\n+        assert_eq!(parse_i64_literal(\"-1UL\"), Some(-1));\n+        assert_eq!(parse_i64_literal(\"0xFFFFFFFFFFFFFFFF\"), Some(-1));\n+        assert_eq!(parse_i64_literal(\"18446744073709551615\"), Some(-1));\n+        assert_eq!(parse_i64_literal(\"-0x8000000000000000\"), Some(i64::MIN));\n+        assert_eq!(parse_u64_literal(\"-1\"), Some(u64::MAX));\n+        assert_eq!(parse_u64_literal(\"1uu\"), None);\n+        assert_eq!(parse_u64_literal(\"1lll\"), None);\n+    }\n+\n+    #[test]\n+    fn parse_constraint_c_literals() {\n+        let doc = \"\\\n+sys_c - Constraint test\n+param: mode\n+  type: int, input\n+  constraint-type: enum(010, 0x10, -07, 0b11, 5U)\n+param: perms\n+  type: uint, input\n+  constraint-type: mask(0755)\n+param: len\n+  type: uint, input\n+  constraint-type: range(01, 0x1000UL)\n+param: align\n+  type: uint, input\n+  constraint-type: alignment(0x10)\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_c\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters;\n+        assert_eq!(p[0].enum_values, [\"8\", \"16\", \"-7\", \"3\", \"5\"]);\n+        assert_eq!(p[1].valid_mask, Some(0o755));\n+        assert_eq!((p[2].min_value, p[2].max_value), (Some(1), Some(4096)));\n+        assert_eq!(p[3].alignment, Some(16));\n+    }\n+\n+    #[test]\n+    fn zero_user_string_limits_are_unset() {\n+        let doc = \"\\\n+sys_s - String test\n+param: name\n+  type: user_ptr, input\n+  range: 0, 255\n+  constraint-type: user_string\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_s\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.constraint_type, 8);\n+        assert_eq!((p.min_value, p.max_value), (None, Some(255)));\n+    }\n+\n+    #[test]\n+    fn user_ptr_type_implies_user_flag() {\n+        let doc = \"\\\n+sys_u - Implicit user flag test\n+param: buf\n+  type: user_ptr, output\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_u\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.param_type, 10); // KAPI_TYPE_USER_PTR\n+        assert_eq!(\n+            p.flags,\n+            (1 \u003c\u003c 1) | (1 \u003c\u003c 6), // OUT | USER\n+            \"user_ptr type must imply KAPI_PARAM_USER\"\n+        );\n+    }\n+\n+    #[test]\n+    fn fd_type_does_not_imply_user_flag() {\n+        // Only user_ptr / path imply KAPI_PARAM_USER. fd, int, uint,\n+        // and every other non-user-space type must leave flags alone.\n+        let doc = \"\\\n+sys_fd - fd has no implicit user flag\n+param: fd\n+  type: fd, input\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_fd\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.param_type, 9);\n+        assert_eq!(p.flags, 1, \"fd must not auto-set KAPI_PARAM_USER\");\n+    }\n+\n+    #[test]\n+    fn path_type_implies_user_flag() {\n+        let doc = \"\\\n+sys_p - Path implicit user flag\n+param: filename\n+  type: path, input\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_p\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.param_type, 11);\n+        assert_eq!(\n+            p.flags,\n+            1 | (1 \u003c\u003c 6), // IN | USER\n+            \"path type must imply KAPI_PARAM_USER\"\n+        );\n+    }\n+\n+    #[test]\n+    fn short_form_enum_equivalence() {\n+        // Short-form and long-form renderings of the same spec must\n+        // produce identical ApiSpec output across every enum family:\n+        // context flags, param type+flags, constraint type, lock type,\n+        // signal direction/action/timing, capability action, side-effect\n+        // bitmask, return check type.\n+        let long = \"\\\n+sys_x - Enum short form test\n+context-flags: KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE\n+\n+param: fd\n+  type: KAPI_TYPE_FD\n+  flags: KAPI_PARAM_IN\n+\n+lock: files-\u003efile_lock\n+  type: KAPI_LOCK_SPINLOCK\n+  scope: acquires\n+  desc: table lock\n+\n+signal: pending_signals\n+  direction: KAPI_SIGNAL_RECEIVE\n+  action: KAPI_SIGNAL_ACTION_RETURN\n+  timing: KAPI_SIGNAL_TIME_DURING\n+  desc: sig\n+\n+capability: CAP_SYS_ADMIN\n+  type: KAPI_CAP_BYPASS_CHECK\n+\n+return:\n+  type: KAPI_TYPE_INT\n+  check-type: KAPI_RETURN_FD\n+  desc: fd or errno\n+\n+side-effect: KAPI_EFFECT_RESOURCE_CREATE | KAPI_EFFECT_ALLOC_MEMORY\n+  target: t\n+  desc: d\n+\";\n+        let short = \"\\\n+sys_x - Enum short form test\n+contexts: process, sleepable\n+\n+param: fd\n+  type: fd, input\n+\n+lock: files-\u003efile_lock\n+  type: spinlock\n+  scope: acquires\n+  desc: table lock\n+\n+signal: pending_signals\n+  direction: receive\n+  action: return\n+  timing: during\n+  desc: sig\n+\n+capability: CAP_SYS_ADMIN\n+  type: bypass_check\n+\n+return:\n+  type: int\n+  check-type: fd\n+  desc: fd or errno\n+\n+side-effect: resource_create | alloc_memory\n+  target: t\n+  desc: d\n+\";\n+        let mut sp_l = parser()\n+            .parse_kerneldoc(long, \"sys_x\", \"syscall\", None)\n+            .unwrap();\n+        let mut sp_s = parser()\n+            .parse_kerneldoc(short, \"sys_x\", \"syscall\", None)\n+            .unwrap();\n+        // The return type name is the human spelling, kept as written.\n+        for sp in [\u0026mut sp_l, \u0026mut sp_s] {\n+            sp.return_spec.as_mut().unwrap().type_name.clear();\n+        }\n+        assert_eq!(\n+            format!(\"{:#?}\", sp_l),\n+            format!(\"{:#?}\", sp_s),\n+            \"long-form and short-form of every enum family must normalise identically\"\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_buffer_short_captures_size_param_idx() {\n+        let doc = \"\\\n+sys_b - Buffer test\n+param: buf\n+  type: user_ptr, output, user\n+  constraint-type: buffer(2)\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_b\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters[0].constraint_type, 11);\n+        assert_eq!(spec.parameters[0].size_param_idx, Some(2));\n+    }\n+\n+    #[test]\n+    fn buffer_short_and_size_param_long_are_symmetric() {\n+        let short = \"\\\n+sys_b - Symmetric buffer test\n+param: buf\n+  type: user_ptr, output, user\n+  constraint-type: buffer(2)\n+\";\n+        let long = \"\\\n+sys_b - Symmetric buffer test\n+param: buf\n+  type: KAPI_TYPE_USER_PTR\n+  flags: KAPI_PARAM_OUT | KAPI_PARAM_USER\n+  constraint-type: KAPI_CONSTRAINT_BUFFER\n+  size-param: 2\n+\";\n+        let sp_s = parser()\n+            .parse_kerneldoc(short, \"sys_b\", \"syscall\", None)\n+            .unwrap();\n+        let sp_l = parser()\n+            .parse_kerneldoc(long, \"sys_b\", \"syscall\", None)\n+            .unwrap();\n+        assert_eq!(format!(\"{:#?}\", sp_s), format!(\"{:#?}\", sp_l));\n+    }\n+\n+    #[test]\n+    fn parse_constraint_type_bare_user_path() {\n+        let doc = \"\\\n+sys_c - Constraint test\n+param: filename\n+  type: path, input, user\n+  constraint-type: user_path\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_c\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters[0].constraint_type, 9); // USER_PATH\n+    }\n+\n+    #[test]\n+    fn starts_subfield_requires_known_key() {\n+        let keys = block_subfield_keys(\u0026BlockContext::SideEffect);\n+        assert!(super::starts_subfield(\"target: file table\", keys));\n+        assert!(super::starts_subfield(\"condition:\", keys));\n+        assert!(super::starts_subfield(\"reversible: yes\", keys));\n+        // A colon inside prose does not open a subfield.\n+        assert!(!super::starts_subfield(\n+            \"this lock mid-operation: the handler\",\n+            keys\n+        ));\n+        assert!(!super::starts_subfield(\"Careful: it is dangerous\", keys));\n+        // A key that belongs to another block type does not count.\n+        assert!(!super::starts_subfield(\"direction: receive\", keys));\n+        assert!(!super::starts_subfield(\"O_RDONLY | O_WRONLY |\", keys));\n+        assert!(!super::starts_subfield(\")\", keys));\n+    }\n+\n+    #[test]\n+    fn multiline_fold_stops_at_sibling_block_attribute() {\n+        // The constraint-type's continuation must not greedily eat the\n+        // next subfield of the same block.\n+        let doc = \"\\\n+sys_y - Fold stop test\n+param: f\n+  type: int, input\n+  constraint-type: mask(FOO |\n+                        BAR)\n+  cdesc: something about f\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_y\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters.len(), 1);\n+        // If the fold over-consumed, `cdesc:` would have been swallowed\n+        // into the mask expression and param.constraint_type would be 0.\n+        assert_eq!(spec.parameters[0].constraint_type, 2);\n+        assert_eq!(\n+            spec.parameters[0].constraint.as_deref(),\n+            Some(\"something about f\")\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_constraint_type_mask_expr_multiline() {\n+        // Real-world sys_open/flags case: a symbolic mask split across\n+        // four continuation lines. The parser must fold the continuation\n+        // lines before running the function-call match, otherwise the\n+        // constraint type silently decays to 0.\n+        let doc = \"\\\n+sys_x - Multi-line mask test\n+param: f\n+  type: int, input\n+  constraint-type: mask(O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_EXCL | O_NOCTTY |\n+                        O_TRUNC | O_APPEND | O_NONBLOCK | O_DSYNC | O_SYNC | FASYNC |\n+                        O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW | O_NOATIME |\n+                        O_CLOEXEC | O_PATH | O_TMPFILE)\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_x\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters.len(), 1);\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.constraint_type, 2, \"multi-line mask must set MASK type\");\n+        // Symbolic mask: must stay unresolved rather than becoming Some(0).\n+        assert_eq!(\n+            p.valid_mask, None,\n+            \"symbolic mask values must remain None, not Some(0)\"\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_constraint_long_form() {\n+        let doc = \"\\\n+sys_c - Constraint test\n+param: foo\n+  type: uint, input\n+  constraint-type: KAPI_CONSTRAINT_MASK\n+  valid-mask: 0xff\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_c\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.constraint_type, 2); // KAPI_CONSTRAINT_MASK\n+    }\n+\n+    #[test]\n+    fn parse_constraint_free_text() {\n+        // `constraint:` carries free-text constraint description;\n+        // function-call short form lives on `constraint-type:`.\n+        let doc = \"\\\n+sys_c - Constraint test\n+param: foo\n+  type: uint, input\n+  constraint: must be a valid page descriptor\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_c\", \"syscall\", None)\n+            .unwrap();\n+\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.constraint_type, 0);\n+        assert_eq!(\n+            p.constraint.as_deref(),\n+            Some(\"must be a valid page descriptor\")\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_description_alias_overrides_kerneldoc() {\n+        // `description:` inside a `param:` block is an alias for `desc:`\n+        // and overrides the @param description.\n+        let doc = \"\\\n+sys_d - Description alias test\n+@size: kerneldoc short description\n+param: size\n+  type: uint, input\n+  description: The new long form description.\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_d\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters.len(), 1);\n+        assert_eq!(\n+            spec.parameters[0].description,\n+            \"The new long form description.\"\n+        );\n+    }\n+\n+    #[test]\n+    fn canonical_equivalence_short_vs_long() {\n+        // Two spellings of the same spec must produce identical ApiSpec\n+        // JSON.\n+        let long = \"\\\n+sys_open - open a file\n+context-flags: KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE\n+\n+param: filename\n+  type: KAPI_TYPE_PATH\n+  flags: KAPI_PARAM_IN | KAPI_PARAM_USER\n+  constraint-type: KAPI_CONSTRAINT_USER_PATH\n+  desc: Pathname to open\n+\n+param: count\n+  type: KAPI_TYPE_UINT\n+  flags: KAPI_PARAM_IN\n+  constraint-type: KAPI_CONSTRAINT_RANGE\n+  range: 0, 4096\n+  desc: Byte count\n+\";\n+        let short = \"\\\n+sys_open - open a file\n+contexts: process, sleepable\n+\n+param: filename\n+  type: path, input, user\n+  constraint-type: user_path\n+  description: Pathname to open\n+\n+param: count\n+  type: uint, input\n+  constraint-type: range(0, 4096)\n+  description: Byte count\n+\";\n+        let long_spec = parser()\n+            .parse_kerneldoc(long, \"sys_open\", \"syscall\", None)\n+            .unwrap();\n+        let short_spec = parser()\n+            .parse_kerneldoc(short, \"sys_open\", \"syscall\", None)\n+            .unwrap();\n+\n+        // ApiSpec isn't Serialize as a whole, so compare the Debug\n+        // rendering, which still proves every field canonicalises\n+        // identically.\n+        let d_long = format!(\"{:#?}\", long_spec);\n+        let d_short = format!(\"{:#?}\", short_spec);\n+        assert_eq!(\n+            d_long, d_short,\n+            \"short-form and long-form specs must normalise identically\"\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_capability_block() {\n+        let doc = \"\\\n+sys_cap - Capability test\n+capability: CAP_SYS_ADMIN\n+  type: required\n+  allows: Full system administration\n+  without: Operation not permitted\n+  condition: always\n+  priority: 5\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_cap\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.capabilities.len(), 1);\n+        let cap = \u0026spec.capabilities[0];\n+        assert_eq!(cap.capability, 21); // CAP_SYS_ADMIN\n+        assert_eq!(cap.action, \"required\");\n+        assert_eq!(cap.allows, \"Full system administration\");\n+        assert_eq!(cap.without_cap, \"Operation not permitted\");\n+        assert_eq!(cap.check_condition.as_deref(), Some(\"always\"));\n+        assert_eq!(cap.priority, Some(5));\n+    }\n+\n+    #[test]\n+    fn parse_lock_block() {\n+        let doc = \"\\\n+sys_lock - Lock test\n+lock: files_lock, KAPI_LOCK_MUTEX\n+  scope: acquires\n+  desc: Protects file table\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_lock\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.locks.len(), 1);\n+        let lock = \u0026spec.locks[0];\n+        assert_eq!(lock.lock_name, \"files_lock\");\n+        assert_eq!(lock.lock_type, 1); // MUTEX\n+        assert_eq!(lock.scope, super::super::KAPI_LOCK_ACQUIRES);\n+        assert_eq!(lock.description, \"Protects file table\");\n+    }\n+\n+    #[test]\n+    fn parse_lock_acquired_released_flags() {\n+        let doc = \"\\\n+sys_lock - Lock test\n+lock: a_lock, KAPI_LOCK_MUTEX\n+  acquired: true\n+  released: true\n+lock: b_lock, KAPI_LOCK_MUTEX\n+  acquired: true\n+lock: c_lock, KAPI_LOCK_MUTEX\n+  released: yes\n+lock: d_lock, KAPI_LOCK_MUTEX\n+  acquired: conditional\n+  released: false\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_lock\", \"syscall\", None)\n+            .unwrap();\n+\n+        let scopes: Vec\u003cu32\u003e = spec.locks.iter().map(|l| l.scope).collect();\n+        assert_eq!(\n+            scopes,\n+            vec![\n+                super::super::KAPI_LOCK_INTERNAL,\n+                super::super::KAPI_LOCK_ACQUIRES,\n+                super::super::KAPI_LOCK_RELEASES,\n+                super::super::KAPI_LOCK_INTERNAL,\n+            ]\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_signal_block() {\n+        let doc = \"\\\n+sys_sig - Signal test\n+signal: SIGKILL\n+  direction: KAPI_SIGNAL_RECEIVE\n+  action: KAPI_SIGNAL_ACTION_TERMINATE\n+  timing: KAPI_SIGNAL_TIME_DURING\n+  priority: 3\n+  restartable: yes\n+  interruptible: yes\n+  desc: Process termination signal\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_sig\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.signals.len(), 1);\n+        let sig = \u0026spec.signals[0];\n+        assert_eq!(sig.signal_name, \"SIGKILL\");\n+        assert_eq!(sig.direction, 1); // RECEIVE\n+        assert_eq!(sig.action, 1); // TERMINATE\n+        assert_eq!(sig.timing, 1); // DURING\n+        assert_eq!(sig.priority, 3);\n+        assert!(sig.restartable);\n+        assert!(sig.interruptible);\n+        assert_eq!(\n+            sig.description.as_deref(),\n+            Some(\"Process termination signal\")\n+        );\n+    }\n+\n+    #[test]\n+    fn parse_signal_errno_shapes() {\n+        // All three accepted spellings of the signal errno field must\n+        // produce the same negative kernel return code.\n+        for (form, label) in [\n+            (\"errno: -EINTR\", \"-EINTR symbolic\"),\n+            (\"errno: EINTR\", \"bare symbolic\"),\n+            (\"errno: -4\", \"numeric literal\"),\n+        ] {\n+            let doc = format!(\n+                \"sys_s - Signal errno test\\n\\\n+                 signal: SIGINT\\n\\\n+                 \\x20 direction: receive\\n\\\n+                 \\x20 action: return\\n\\\n+                 \\x20 {}\\n\",\n+                form,\n+            );\n+            let spec = parser()\n+                .parse_kerneldoc(\u0026doc, \"sys_s\", \"syscall\", None)\n+                .unwrap();\n+            assert_eq!(spec.signals.len(), 1, \"{label}\");\n+            assert_eq!(\n+                spec.signals[0].error_on_signal,\n+                Some(-4),\n+                \"errno form {label:?} must resolve to -EINTR (-4)\",\n+            );\n+        }\n+    }\n+\n+    #[test]\n+    fn parse_side_effect_flat() {\n+        let doc = \"\\\n+sys_se - Side effect test\n+side-effect: KAPI_EFFECT_MODIFY_STATE, file_table, Allocates a new file descriptor\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_se\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.side_effects.len(), 1);\n+        let se = \u0026spec.side_effects[0];\n+        assert_eq!(se.effect_type, 1 \u003c\u003c 2); // KAPI_EFFECT_MODIFY_STATE\n+        assert_eq!(se.target, \"file_table\");\n+        assert_eq!(se.description, \"Allocates a new file descriptor\");\n+    }\n+\n+    #[test]\n+    fn parse_side_effect_block() {\n+        let doc = \"\\\n+sys_se2 - Side effect block test\n+side-effect: KAPI_EFFECT_ALLOC_MEMORY\n+  target: kernel_heap\n+  desc: Allocates kernel memory\n+  reversible: yes\n+  condition: size \u003e 0\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_se2\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.side_effects.len(), 1);\n+        let se = \u0026spec.side_effects[0];\n+        assert_eq!(se.effect_type, 1 \u003c\u003c 0); // KAPI_EFFECT_ALLOC_MEMORY\n+        assert_eq!(se.target, \"kernel_heap\");\n+        assert_eq!(se.description, \"Allocates kernel memory\");\n+        assert!(se.reversible);\n+        assert_eq!(se.condition.as_deref(), Some(\"size \u003e 0\"));\n+    }\n+\n+    #[test]\n+    fn parse_empty_doc_no_error() {\n+        let doc = \"\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_empty\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.name, \"sys_empty\");\n+        assert!(spec.description.is_none());\n+        assert!(spec.parameters.is_empty());\n+        assert!(spec.errors.is_empty());\n+        assert!(spec.signals.is_empty());\n+        assert!(spec.capabilities.is_empty());\n+        assert!(spec.locks.is_empty());\n+        assert!(spec.side_effects.is_empty());\n+        assert!(spec.context_flags.is_empty());\n+    }\n+\n+    #[test]\n+    fn parse_missing_sections_no_error() {\n+        // Only has a description, no KAPI annotations\n+        let doc = \"\\\n+sys_simple - Just a simple syscall\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_simple\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.description.as_deref(), Some(\"Just a simple syscall\"));\n+        assert!(spec.parameters.is_empty());\n+        assert!(spec.errors.is_empty());\n+        assert!(spec.context_flags.is_empty());\n+    }\n+\n+    #[test]\n+    fn parse_constraint_block() {\n+        let doc = \"\\\n+sys_cst - Constraint test\n+constraint: valid_fd\n+  desc: File descriptor must be valid and open\n+  expr: fd \u003e= 0 \u0026\u0026 fd \u003c NR_OPEN\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_cst\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.constraints.len(), 1);\n+        let cst = \u0026spec.constraints[0];\n+        assert_eq!(cst.name, \"valid_fd\");\n+        assert_eq!(cst.description, \"File descriptor must be valid and open\");\n+        assert_eq!(cst.expression.as_deref(), Some(\"fd \u003e= 0 \u0026\u0026 fd \u003c NR_OPEN\"));\n+    }\n+\n+    #[test]\n+    fn parse_state_transition_flat() {\n+        let doc = \"\\\n+sys_st - State transition test\n+state-trans: fd, open, closed, File descriptor is closed\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_st\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.state_transitions.len(), 1);\n+        let st = \u0026spec.state_transitions[0];\n+        assert_eq!(st.object, \"fd\");\n+        assert_eq!(st.from_state, \"open\");\n+        assert_eq!(st.to_state, \"closed\");\n+        assert_eq!(st.description, \"File descriptor is closed\");\n+    }\n+\n+    #[test]\n+    fn parse_param_block_with_range() {\n+        let doc = \"\\\n+sys_rng - Range test\n+@count: byte count\n+param: count\n+  type: KAPI_TYPE_UINT\n+  flags: IN\n+  range: 0, 4096\n+  constraint-type: KAPI_CONSTRAINT_RANGE\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_rng\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.parameters.len(), 1);\n+        let p = \u0026spec.parameters[0];\n+        assert_eq!(p.name, \"count\");\n+        assert_eq!(p.param_type, 2); // UINT\n+        assert_eq!(p.flags, 1); // IN\n+        assert_eq!(p.min_value, Some(0));\n+        assert_eq!(p.max_value, Some(4096));\n+        assert_eq!(p.constraint_type, 1); // RANGE\n+    }\n+\n+    #[test]\n+    fn parse_return_block() {\n+        let doc = \"\\\n+sys_ret - Return test\n+return:\n+  type: KAPI_TYPE_INT\n+  check-type: KAPI_RETURN_FD\n+  success: 0\n+  desc: Returns file descriptor on success\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_ret\", \"syscall\", None)\n+            .unwrap();\n+\n+        let ret = spec.return_spec.as_ref().unwrap();\n+        assert_eq!(ret.type_name, \"KAPI_TYPE_INT\");\n+        assert_eq!(ret.return_type, 1); // INT\n+        assert_eq!(ret.check_type, 3); // FD\n+        assert_eq!(ret.success_value, None); // FD checks carry no success value\n+        assert_eq!(ret.description, \"Returns file descriptor on success\");\n+    }\n+\n+    #[test]\n+    fn colon_in_continuation_line_does_not_start_subfield() {\n+        let doc = \"\\\n+sys_col - Colon continuation test\n+param: x\n+  type: int\n+  cdesc: Must be sane. Note: this is a continuation\n+    with a colon: in the middle.\n+\n+lock: mylock\n+  type: mutex\n+  desc: Taken at entry; if the caller drops\n+    this lock mid-operation: the handler waits\n+    until released.\n+\n+side-effect: modify_state\n+  target: stuff\n+  condition: only when a\n+    special case applies: for example\n+    on Tuesdays\n+  desc: Does things.\n+    Careful: it is dangerous.\n+  reversible: no\n+\n+signal: SIGINT\n+  direction: receive\n+  condition: while blocked, unless\n+    flagged: see below\n+  desc: Interrupts the wait.\n+\n+capability: CAP_SYS_ADMIN\n+  type: bypass_check\n+  allows: Things\n+    that need: privilege\n+  condition: Always\n+\n+constraint: limit\n+  desc: A limit\n+    of note: nothing.\n+  expr: a \u0026\u0026\n+    b\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_col\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(\n+            spec.parameters[0].constraint.as_deref(),\n+            Some(\"Must be sane. Note: this is a continuation with a colon: in the middle.\")\n+        );\n+        assert_eq!(\n+            spec.locks[0].description,\n+            \"Taken at entry; if the caller drops this lock mid-operation: the handler waits until released.\"\n+        );\n+        let se = \u0026spec.side_effects[0];\n+        assert_eq!(\n+            se.condition.as_deref(),\n+            Some(\"only when a special case applies: for example on Tuesdays\")\n+        );\n+        assert_eq!(se.description, \"Does things. Careful: it is dangerous.\");\n+        assert!(!se.reversible);\n+        let sig = \u0026spec.signals[0];\n+        assert_eq!(\n+            sig.condition.as_deref(),\n+            Some(\"while blocked, unless flagged: see below\")\n+        );\n+        assert_eq!(sig.description.as_deref(), Some(\"Interrupts the wait.\"));\n+        let cap = \u0026spec.capabilities[0];\n+        assert_eq!(cap.allows, \"Things that need: privilege\");\n+        assert_eq!(cap.check_condition.as_deref(), Some(\"Always\"));\n+        let cst = \u0026spec.constraints[0];\n+        assert_eq!(cst.description, \"A limit of note: nothing.\");\n+        assert_eq!(cst.expression.as_deref(), Some(\"a \u0026\u0026 b\"));\n+    }\n+\n+    #[test]\n+    fn multiline_side_effect_condition_is_kept_whole() {\n+        let doc = \"\\\n+sys_mc - Multi-line condition test\n+side-effect: modify_state\n+  target: userfaultfd event queue\n+  condition: MADV_DONTNEED, MADV_FREE\n+    on a VMA whose userfaultfd context negotiated\n+    UFFD_FEATURE_EVENT_REMOVE\n+  desc: Generates a notification.\n+    The monitor cannot veto it.\n+  reversible: no\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_mc\", \"syscall\", None)\n+            .unwrap();\n+\n+        let se = \u0026spec.side_effects[0];\n+        assert_eq!(\n+            se.condition.as_deref(),\n+            Some(\n+                \"MADV_DONTNEED, MADV_FREE on a VMA whose userfaultfd context \\\n+                 negotiated UFFD_FEATURE_EVENT_REMOVE\"\n+            )\n+        );\n+        assert_eq!(\n+            se.description,\n+            \"Generates a notification. The monitor cannot veto it.\"\n+        );\n+    }\n+\n+    #[test]\n+    fn blank_line_inside_block_continues_subfield() {\n+        let doc = \"\\\n+sys_bl - Blank line test\n+lock: l\n+  type: mutex\n+  desc: First half\n+\n+    second half.\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_bl\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.locks[0].description, \"First half second half.\");\n+    }\n+\n+    #[test]\n+    fn param_cdesc_is_the_constraint_text() {\n+        let doc = \"\\\n+sys_cd - cdesc test\n+param: fd\n+  type: int, input\n+  cdesc: Must be open.\n+    Zero is allowed.\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_cd\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(\n+            spec.parameters[0].constraint.as_deref(),\n+            Some(\"Must be open. Zero is allowed.\")\n+        );\n+    }\n+\n+    #[test]\n+    fn state_transition_block_keeps_condition_separate() {\n+        let doc = \"\\\n+sys_stb - State transition block test\n+state-trans: file_descriptor\n+  from: open\n+  to: closed/free\n+  condition: Valid fd passed to close\n+  desc: The fd becomes unusable.\n+    It may be reused.\n+\n+state-trans: refcount\n+  from: n\n+  to: n-1\n+  desc: Decremented.\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_stb\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(spec.state_transitions.len(), 2);\n+        let st = \u0026spec.state_transitions[0];\n+        assert_eq!(st.object, \"file_descriptor\");\n+        assert_eq!(st.from_state, \"open\");\n+        assert_eq!(st.to_state, \"closed/free\");\n+        assert_eq!(st.condition.as_deref(), Some(\"Valid fd passed to close\"));\n+        assert_eq!(st.description, \"The fd becomes unusable. It may be reused.\");\n+        let st = \u0026spec.state_transitions[1];\n+        assert_eq!(st.object, \"refcount\");\n+        assert_eq!(st.condition, None);\n+        assert_eq!(st.description, \"Decremented.\");\n+    }\n+\n+    #[test]\n+    fn state_transition_flat_description_keeps_commas() {\n+        let doc = \"\\\n+sys_stc - Flat state transition test\n+state-trans: fd, open, closed, File is closed, and the number is free\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_stc\", \"syscall\", None)\n+            .unwrap();\n+\n+        let st = \u0026spec.state_transitions[0];\n+        assert_eq!(st.description, \"File is closed, and the number is free\");\n+        assert_eq!(st.condition, None);\n+    }\n+\n+    #[test]\n+    fn return_type_name_is_kept_as_written() {\n+        for (written, ty) in [(\"int\", 1), (\"long\", 0), (\"KAPI_TYPE_UINT\", 2)] {\n+            let doc = format!(\"sys_rt - Return type test\\nreturn:\\n  type: {written}\\n\");\n+            let spec = parser()\n+                .parse_kerneldoc(\u0026doc, \"sys_rt\", \"syscall\", None)\n+                .unwrap();\n+            let ret = spec.return_spec.as_ref().unwrap();\n+            assert_eq!(ret.type_name, written);\n+            assert_eq!(ret.return_type, ty);\n+        }\n+    }\n+\n+    #[test]\n+    fn return_success_follows_check_type() {\n+        let cases = [\n+            // (check-type, success, value, min, max)\n+            (\"exact\", \"0\", Some(0), None, None),\n+            (\"exact\", \"= 0\", Some(0), None, None),\n+            (\"exact\", \"== -1\", Some(-1), None, None),\n+            (\"exact\", \"0x10\", Some(16), None, None),\n+            (\"exact\", \"\u003e= 0\", None, None, None),\n+            (\"range\", \"\u003e= 0\", None, Some(0), Some(i64::MAX)),\n+            (\"range\", \"\u003e= 1\", None, Some(1), Some(i64::MAX)),\n+            (\"range\", \"0\", None, Some(0), Some(i64::MAX)),\n+            (\"fd\", \"\u003e= 0\", None, None, None),\n+            (\"error_check\", \"0\", None, None, None),\n+        ];\n+        for (check, success, value, min, max) in cases {\n+            let doc = format!(\n+                \"sys_rs - Return success test\\nreturn:\\n  type: int\\n  \\\n+                 success: {success}\\n  check-type: {check}\\n  desc: ok\\n\"\n+            );\n+            let spec = parser()\n+                .parse_kerneldoc(\u0026doc, \"sys_rs\", \"syscall\", None)\n+                .unwrap();\n+            let ret = spec.return_spec.as_ref().unwrap();\n+            assert_eq!(ret.success_value, value, \"{check} {success}\");\n+            assert_eq!(ret.success_min, min, \"{check} {success}\");\n+            assert_eq!(ret.success_max, max, \"{check} {success}\");\n+        }\n+    }\n+\n+    #[test]\n+    fn return_description_continuation_with_colon() {\n+        let doc = \"\\\n+sys_rd - Return description test\n+return:\n+  type: int\n+  check-type: exact\n+  success: 0\n+  desc: Returns zero on success. Note: this is\n+    a continuation line with a colon.\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_rd\", \"syscall\", None)\n+            .unwrap();\n+\n+        let ret = spec.return_spec.as_ref().unwrap();\n+        assert_eq!(\n+            ret.description,\n+            \"Returns zero on success. Note: this is a continuation line with a colon.\"\n+        );\n+        assert_eq!(ret.success_value, Some(0));\n+    }\n+\n+    #[test]\n+    fn examples_keep_one_example_per_line() {\n+        let doc = \"\\\n+sys_ex - Examples test\n+examples: 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 \u003c len) {\n+    n = write(fd, buf + total, len - total);\n+    if (n \u003c 0) break;\n+  }\n+\n+  close(fd);\n+\n+notes: After the examples.\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_ex\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(\n+            spec.examples.as_deref(),\n+            Some(\n+                \"fd = open(\\\"/etc/passwd\\\", O_RDONLY);  // Read existing file\\n\\\n+                 fd = open(\\\"/tmp/new\\\", O_WRONLY | O_CREAT, 0644);  // Create\\n\\\n+                 // Handle short writes:\\n\\\n+                 while (total \u003c len) {\\n  \\\n+                 n = write(fd, buf + total, len - total);\\n  \\\n+                 if (n \u003c 0) break;\\n\\\n+                 }\\n\\\n+                 \\n\\\n+                 close(fd);\"\n+            )\n+        );\n+        assert_eq!(spec.notes.as_deref(), Some(\"After the examples.\"));\n+    }\n+\n+    #[test]\n+    fn notes_and_long_desc_keep_paragraphs_and_bullets() {\n+        let doc = \"\\\n+sys_nt - Notes test\n+long-desc: First paragraph wraps\n+  over two lines.\n+\n+  Second paragraph introduces a list:\n+  - item one wraps\n+    onto a second line\n+  - item two\n+\n+  Last paragraph.\n+notes: The behavior varies:\n+\n+  - Regular files: reads\n+    from the current position.\n+\n+  - Pipes: block.\n+\n+  Trailing: text.\n+\";\n+        let spec = parser()\n+            .parse_kerneldoc(doc, \"sys_nt\", \"syscall\", None)\n+            .unwrap();\n+\n+        assert_eq!(\n+            spec.long_description.as_deref(),\n+            Some(\n+                \"First paragraph wraps over two lines.\\n\\n\\\n+                 Second paragraph introduces a list:\\n\\\n+                 - item one wraps onto a second line\\n\\\n+                 - item two\\n\\n\\\n+                 Last paragraph.\"\n+            )\n+        );\n+        assert_eq!(\n+            spec.notes.as_deref(),\n+            Some(\n+                \"The behavior varies:\\n\\n\\\n+                 - Regular files: reads from the current position.\\n\\n\\\n+                 - Pipes: block.\\n\\n\\\n+                 Trailing: text.\"\n+            )\n+        );\n+    }\n+\n+    #[test]\n+    fn fold_lines_handles_leading_blank_and_tabs() {\n+        assert_eq!(\n+            super::fold_lines(\u0026[\"\", \"  a();\", \"  b();\", \"    c();\", \"\"]),\n+            \"a();\\nb();\\n  c();\"\n+        );\n+        assert_eq!(super::fold_lines(\u0026[\"x();\", \"\\ty();\"]), \"x();\\ny();\");\n+        assert_eq!(super::fold_lines(\u0026[\"\"]), \"\");\n+        assert_eq!(super::fold_lines(\u0026[\"a\", \"  b\", \"\", \"\", \"  c\"]), \"a\\nb\\n\\nc\");\n+    }\n+\n+    #[test]\n+    fn fold_paragraphs_joins_wrapped_lines_only() {\n+        assert_eq!(\n+            super::fold_paragraphs(\u0026[\"one\", \"  two\", \"\", \"\", \"  three\", \"- four\", \"- five\"]),\n+            \"one two\\n\\nthree\\n- four\\n- five\"\n+        );\n+        assert_eq!(super::fold_paragraphs(\u0026[\"\", \"  \"]), \"\");\n+    }\n+}\ndiff --git a/tools/kapi/src/extractor/mod.rs b/tools/kapi/src/extractor/mod.rs\nnew file mode 100644\nindex 0000000000000..d7ada57924d7a\n--- /dev/null\n+++ b/tools/kapi/src/extractor/mod.rs\n@@ -0,0 +1,442 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use crate::formatter::OutputFormatter;\n+use anyhow::Result;\n+use std::io::Write;\n+\n+pub mod debugfs;\n+pub mod kerneldoc_parser;\n+pub mod source_parser;\n+pub mod vmlinux;\n+\n+pub use debugfs::DebugfsExtractor;\n+pub use source_parser::SourceExtractor;\n+pub use vmlinux::VmlinuxExtractor;\n+\n+/// Capability specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct CapabilitySpec {\n+    pub capability: i32,\n+    pub name: String,\n+    pub action: String,\n+    pub allows: String,\n+    pub without_cap: String,\n+    pub check_condition: Option\u003cString\u003e,\n+    pub priority: Option\u003cu8\u003e,\n+    pub alternatives: Vec\u003ci32\u003e,\n+}\n+\n+/// Parameter specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct ParamSpec {\n+    pub index: u32,\n+    pub name: String,\n+    pub type_name: String,\n+    pub description: String,\n+    pub flags: u32,\n+    pub param_type: u32,\n+    pub constraint_type: u32,\n+    pub constraint: Option\u003cString\u003e,\n+    pub min_value: Option\u003ci64\u003e,\n+    pub max_value: Option\u003ci64\u003e,\n+    pub valid_mask: Option\u003cu64\u003e,\n+    pub enum_values: Vec\u003cString\u003e,\n+    pub size: Option\u003cu32\u003e,\n+    pub alignment: Option\u003cu32\u003e,\n+    /// Index of the parameter that carries this parameter's byte count\n+    /// (for KAPI_CONSTRAINT_BUFFER). Populated by either\n+    /// `size-param: N` (long form) or `constraint-type: buffer(N)`\n+    /// (short form).\n+    pub size_param_idx: Option\u003cu32\u003e,\n+}\n+\n+/// Constraint type enum values matching kernel enum kapi_constraint_type\n+pub const KAPI_CONSTRAINT_RANGE: u32 = 1;\n+pub const KAPI_CONSTRAINT_MASK: u32 = 2;\n+pub const KAPI_CONSTRAINT_USER_STRING: u32 = 8;\n+\n+impl ParamSpec {\n+    /// Clear the numeric fields the constraint type does not use. The\n+    /// compiled struct always carries them, so an unset value reads as 0.\n+    pub fn keep_used_numbers(\u0026mut self) {\n+        match self.constraint_type {\n+            KAPI_CONSTRAINT_RANGE =\u003e {}\n+            KAPI_CONSTRAINT_USER_STRING =\u003e self.drop_unset_string_limits(),\n+            _ =\u003e {\n+                self.min_value = None;\n+                self.max_value = None;\n+            }\n+        }\n+        if self.constraint_type != KAPI_CONSTRAINT_MASK {\n+            self.valid_mask = None;\n+        }\n+        self.size = self.size.filter(|\u0026n| n != 0);\n+        self.alignment = self.alignment.filter(|\u0026n| n != 0);\n+    }\n+\n+    /// The kernel treats a user_string length limit of 0 as no limit.\n+    pub fn drop_unset_string_limits(\u0026mut self) {\n+        if self.constraint_type == KAPI_CONSTRAINT_USER_STRING {\n+            self.min_value = self.min_value.filter(|\u0026n| n \u003e 0);\n+            self.max_value = self.max_value.filter(|\u0026n| n \u003e 0);\n+        }\n+    }\n+}\n+\n+/// Return value specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct ReturnSpec {\n+    pub type_name: String,\n+    pub description: String,\n+    pub return_type: u32,\n+    pub check_type: u32,\n+    pub success_value: Option\u003ci64\u003e,\n+    pub success_min: Option\u003ci64\u003e,\n+    pub success_max: Option\u003ci64\u003e,\n+    pub error_values: Vec\u003ci32\u003e,\n+}\n+\n+impl ReturnSpec {\n+    /// Drop the success fields the check type does not use, the way the\n+    /// generated KAPI_RETURN_SUCCESS()/KAPI_RETURN_SUCCESS_RANGE() macros\n+    /// only set the one that applies.\n+    pub fn keep_used_success_fields(\u0026mut self) {\n+        match self.check_type {\n+            0 =\u003e {\n+                self.success_min = None;\n+                self.success_max = None;\n+            }\n+            1 =\u003e self.success_value = None,\n+            _ =\u003e {\n+                self.success_value = None;\n+                self.success_min = None;\n+                self.success_max = None;\n+            }\n+        }\n+    }\n+}\n+\n+/// Error specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct ErrorSpec {\n+    pub error_code: i32,\n+    pub name: String,\n+    pub condition: String,\n+    pub description: String,\n+}\n+\n+/// Signal specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct SignalSpec {\n+    pub signal_num: i32,\n+    pub signal_name: String,\n+    pub direction: u32,\n+    pub action: u32,\n+    pub target: Option\u003cString\u003e,\n+    pub condition: Option\u003cString\u003e,\n+    pub description: Option\u003cString\u003e,\n+    pub timing: u32,\n+    pub priority: u32,\n+    pub restartable: bool,\n+    pub interruptible: bool,\n+    pub queue: Option\u003cString\u003e,\n+    pub sa_flags: u32,\n+    pub sa_flags_required: u32,\n+    pub sa_flags_forbidden: u32,\n+    pub state_required: u32,\n+    pub state_forbidden: u32,\n+    pub error_on_signal: Option\u003ci32\u003e,\n+    /// Signal number to transform to (e.g. `SIGKILL` -\u003e 9 on x86).\n+    /// Always an integer or null in JSON -- the schema never widens to\n+    /// a string. Extractors reading the compiled struct (`--vmlinux`,\n+    /// `--debugfs`) populate this directly. The source-kerneldoc parser\n+    /// populates it only when the `transform-to:` subfield is a numeric\n+    /// literal; symbolic signal names are arch-dependent and cannot be\n+    /// resolved portably in userspace, so they are reported via an\n+    /// stderr warning and leave this field `None`. Consumers that need\n+    /// the resolved number for a symbolic spec should use `--vmlinux`\n+    /// or `--debugfs` against a kernel built for the target arch.\n+    pub transform_to: Option\u003ci32\u003e,\n+}\n+\n+/// Signal mask specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct SignalMaskSpec {\n+    pub name: String,\n+    pub description: String,\n+    pub signals: Vec\u003ci32\u003e,\n+}\n+\n+/// Side effect specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct SideEffectSpec {\n+    pub effect_type: u32,\n+    pub target: String,\n+    pub condition: Option\u003cString\u003e,\n+    pub description: String,\n+    pub reversible: bool,\n+}\n+\n+/// State transition specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct StateTransitionSpec {\n+    pub object: String,\n+    pub from_state: String,\n+    pub to_state: String,\n+    pub condition: Option\u003cString\u003e,\n+    pub description: String,\n+}\n+\n+/// Constraint specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct ConstraintSpec {\n+    pub name: String,\n+    pub description: String,\n+    pub expression: Option\u003cString\u003e,\n+}\n+\n+/// Lock scope enum values matching kernel enum kapi_lock_scope\n+pub const KAPI_LOCK_INTERNAL: u32 = 0;\n+pub const KAPI_LOCK_ACQUIRES: u32 = 1;\n+pub const KAPI_LOCK_RELEASES: u32 = 2;\n+pub const KAPI_LOCK_CALLER_HELD: u32 = 3;\n+\n+/// Lock specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct LockSpec {\n+    pub lock_name: String,\n+    pub lock_type: u32,\n+    pub scope: u32,\n+    pub description: String,\n+}\n+\n+/// Struct field specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct StructFieldSpec {\n+    pub name: String,\n+    pub field_type: u32,\n+    pub type_name: String,\n+    pub offset: usize,\n+    pub size: usize,\n+    pub flags: u32,\n+    pub constraint_type: u32,\n+    pub min_value: i64,\n+    pub max_value: i64,\n+    pub valid_mask: u64,\n+    pub description: String,\n+}\n+\n+/// Struct specification\n+#[derive(Debug, Clone, serde::Serialize)]\n+pub struct StructSpec {\n+    pub name: String,\n+    pub size: usize,\n+    pub alignment: usize,\n+    pub field_count: u32,\n+    pub fields: Vec\u003cStructFieldSpec\u003e,\n+    pub description: String,\n+}\n+\n+/// Common API specification information that all extractors should provide\n+#[derive(Debug, Clone, Default)]\n+pub struct ApiSpec {\n+    pub name: String,\n+    pub api_type: String,\n+    pub description: Option\u003cString\u003e,\n+    pub long_description: Option\u003cString\u003e,\n+    pub version: Option\u003cString\u003e,\n+    pub context_flags: Vec\u003cString\u003e,\n+    pub param_count: Option\u003cu32\u003e,\n+    pub error_count: Option\u003cu32\u003e,\n+    pub examples: Option\u003cString\u003e,\n+    pub notes: Option\u003cString\u003e,\n+    // Sysfs-specific fields\n+    pub subsystem: Option\u003cString\u003e,\n+    pub sysfs_path: Option\u003cString\u003e,\n+    pub permissions: Option\u003cString\u003e,\n+    pub capabilities: Vec\u003cCapabilitySpec\u003e,\n+    pub parameters: Vec\u003cParamSpec\u003e,\n+    pub return_spec: Option\u003cReturnSpec\u003e,\n+    pub errors: Vec\u003cErrorSpec\u003e,\n+    pub signals: Vec\u003cSignalSpec\u003e,\n+    pub signal_masks: Vec\u003cSignalMaskSpec\u003e,\n+    pub side_effects: Vec\u003cSideEffectSpec\u003e,\n+    pub state_transitions: Vec\u003cStateTransitionSpec\u003e,\n+    pub constraints: Vec\u003cConstraintSpec\u003e,\n+    pub locks: Vec\u003cLockSpec\u003e,\n+    pub struct_specs: Vec\u003cStructSpec\u003e,\n+}\n+\n+/// Trait for extracting API specifications from different sources\n+pub trait ApiExtractor {\n+    /// Extract all API specifications from the source\n+    fn extract_all(\u0026self) -\u003e Result\u003cVec\u003cApiSpec\u003e\u003e;\n+\n+    /// Extract a specific API specification by name\n+    fn extract_by_name(\u0026self, name: \u0026str) -\u003e Result\u003cOption\u003cApiSpec\u003e\u003e;\n+\n+    /// Display detailed information about a specific API\n+    fn display_api_details(\n+        \u0026self,\n+        api_name: \u0026str,\n+        formatter: \u0026mut dyn OutputFormatter,\n+        writer: \u0026mut dyn Write,\n+    ) -\u003e Result\u003c()\u003e;\n+}\n+\n+/// Helper function to display an ApiSpec using a formatter\n+pub fn display_api_spec(\n+    spec: \u0026ApiSpec,\n+    formatter: \u0026mut dyn OutputFormatter,\n+    writer: \u0026mut dyn Write,\n+) -\u003e Result\u003c()\u003e {\n+    formatter.begin_api_details(writer, \u0026spec.name)?;\n+\n+    if let Some(desc) = \u0026spec.description {\n+        formatter.description(writer, desc)?;\n+    }\n+\n+    if let Some(long_desc) = \u0026spec.long_description {\n+        formatter.long_description(writer, long_desc)?;\n+    }\n+\n+    if !spec.context_flags.is_empty() {\n+        formatter.begin_context_flags(writer)?;\n+        for flag in \u0026spec.context_flags {\n+            formatter.context_flag(writer, flag)?;\n+        }\n+        formatter.end_context_flags(writer)?;\n+    }\n+\n+    if !spec.parameters.is_empty() {\n+        formatter.begin_parameters(writer, spec.parameters.len().try_into().unwrap_or(u32::MAX))?;\n+        for param in \u0026spec.parameters {\n+            formatter.parameter(writer, param)?;\n+        }\n+        formatter.end_parameters(writer)?;\n+    }\n+\n+    if let Some(ret) = \u0026spec.return_spec {\n+        formatter.return_spec(writer, ret)?;\n+    }\n+\n+    if !spec.errors.is_empty() {\n+        formatter.begin_errors(writer, spec.errors.len().try_into().unwrap_or(u32::MAX))?;\n+        for error in \u0026spec.errors {\n+            formatter.error(writer, error)?;\n+        }\n+        formatter.end_errors(writer)?;\n+    }\n+\n+    if let Some(notes) = \u0026spec.notes {\n+        formatter.notes(writer, notes)?;\n+    }\n+\n+    if let Some(examples) = \u0026spec.examples {\n+        formatter.examples(writer, examples)?;\n+    }\n+\n+    // Display sysfs-specific fields\n+    if spec.api_type == \"sysfs\" {\n+        if let Some(subsystem) = \u0026spec.subsystem {\n+            formatter.sysfs_subsystem(writer, subsystem)?;\n+        }\n+        if let Some(path) = \u0026spec.sysfs_path {\n+            formatter.sysfs_path(writer, path)?;\n+        }\n+        if let Some(perms) = \u0026spec.permissions {\n+            formatter.sysfs_permissions(writer, perms)?;\n+        }\n+    }\n+\n+    if !spec.capabilities.is_empty() {\n+        formatter.begin_capabilities(writer)?;\n+        for cap in \u0026spec.capabilities {\n+            formatter.capability(writer, cap)?;\n+        }\n+        formatter.end_capabilities(writer)?;\n+    }\n+\n+    // Display signals\n+    if !spec.signals.is_empty() {\n+        formatter.begin_signals(writer, spec.signals.len().try_into().unwrap_or(u32::MAX))?;\n+        for signal in \u0026spec.signals {\n+            formatter.signal(writer, signal)?;\n+        }\n+        formatter.end_signals(writer)?;\n+    }\n+\n+    // Display signal masks\n+    if !spec.signal_masks.is_empty() {\n+        formatter.begin_signal_masks(\n+            writer,\n+            spec.signal_masks.len().try_into().unwrap_or(u32::MAX),\n+        )?;\n+        for mask in \u0026spec.signal_masks {\n+            formatter.signal_mask(writer, mask)?;\n+        }\n+        formatter.end_signal_masks(writer)?;\n+    }\n+\n+    // Display side effects\n+    if !spec.side_effects.is_empty() {\n+        formatter.begin_side_effects(\n+            writer,\n+            spec.side_effects.len().try_into().unwrap_or(u32::MAX),\n+        )?;\n+        for effect in \u0026spec.side_effects {\n+            formatter.side_effect(writer, effect)?;\n+        }\n+        formatter.end_side_effects(writer)?;\n+    }\n+\n+    // Display state transitions\n+    if !spec.state_transitions.is_empty() {\n+        formatter.begin_state_transitions(\n+            writer,\n+            spec.state_transitions.len().try_into().unwrap_or(u32::MAX),\n+        )?;\n+        for trans in \u0026spec.state_transitions {\n+            formatter.state_transition(writer, trans)?;\n+        }\n+        formatter.end_state_transitions(writer)?;\n+    }\n+\n+    // Display constraints\n+    if !spec.constraints.is_empty() {\n+        formatter.begin_constraints(\n+            writer,\n+            spec.constraints.len().try_into().unwrap_or(u32::MAX),\n+        )?;\n+        for constraint in \u0026spec.constraints {\n+            formatter.constraint(writer, constraint)?;\n+        }\n+        formatter.end_constraints(writer)?;\n+    }\n+\n+    // Display locks\n+    if !spec.locks.is_empty() {\n+        formatter.begin_locks(writer, spec.locks.len().try_into().unwrap_or(u32::MAX))?;\n+        for lock in \u0026spec.locks {\n+            formatter.lock(writer, lock)?;\n+        }\n+        formatter.end_locks(writer)?;\n+    }\n+\n+    // Display struct specs\n+    if !spec.struct_specs.is_empty() {\n+        formatter.begin_struct_specs(\n+            writer,\n+            spec.struct_specs.len().try_into().unwrap_or(u32::MAX),\n+        )?;\n+        for struct_spec in \u0026spec.struct_specs {\n+            formatter.struct_spec(writer, struct_spec)?;\n+        }\n+        formatter.end_struct_specs(writer)?;\n+    }\n+\n+    formatter.end_api_details(writer)?;\n+\n+    Ok(())\n+}\ndiff --git a/tools/kapi/src/extractor/source_parser.rs b/tools/kapi/src/extractor/source_parser.rs\nnew file mode 100644\nindex 0000000000000..cc50e15f16c83\n--- /dev/null\n+++ b/tools/kapi/src/extractor/source_parser.rs\n@@ -0,0 +1,531 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use super::kerneldoc_parser::KerneldocParser;\n+use super::{display_api_spec, ApiExtractor, ApiSpec};\n+use crate::formatter::OutputFormatter;\n+use anyhow::{bail, Context, Result};\n+use regex::Regex;\n+use std::fs;\n+use std::io::Write;\n+use std::path::Path;\n+use walkdir::WalkDir;\n+\n+/// Extractor for kernel source files with KAPI-annotated kerneldoc\n+pub struct SourceExtractor {\n+    path: String,\n+    parser: KerneldocParser,\n+    syscall_regex: Regex,\n+    ioctl_regex: Regex,\n+    function_regex: Regex,\n+}\n+\n+impl SourceExtractor {\n+    pub fn new(path: \u0026str) -\u003e Result\u003cSelf\u003e {\n+        if !Path::new(path).exists() {\n+            bail!(\"Source path does not exist: {path}\");\n+        }\n+\n+        Ok(SourceExtractor {\n+            path: path.to_string(),\n+            parser: KerneldocParser::new(),\n+            syscall_regex: Regex::new(r\"SYSCALL_DEFINE\\d+\\((\\w+)\")?,\n+            ioctl_regex: Regex::new(r\"(?:static\\s+)?long\\s+(\\w+_ioctl)\\s*\\(\")?,\n+            function_regex: Regex::new(concat!(\n+                r\"(?m)^(?:static\\s+)?(?:inline\\s+)?\",\n+                r\"(?:(?:unsigned\\s+)?\",\n+                r\"(?:long|int|void|char|short\",\n+                r\"|struct\\s+\\w+\\s*\\*?\",\n+                r\"|[\\w_]+_t)\",\n+                r\"\\s*\\*?\\s+)?\",\n+                r\"(\\w+)\\s*\\([^)]*\\)\",\n+            ))?,\n+        })\n+    }\n+\n+    fn extract_from_file(\u0026self, path: \u0026Path) -\u003e Result\u003cVec\u003cApiSpec\u003e\u003e {\n+        let content = fs::read_to_string(path)\n+            .with_context(|| format!(\"Failed to read file: {}\", path.display()))?;\n+\n+        self.extract_from_content(\u0026content)\n+    }\n+\n+    /// Same rule as has-apispec in scripts/Makefile.build: a contexts: (or\n+    /// context-flags:) line plus one more KAPI section.\n+    fn has_context_line(doc: \u0026str) -\u003e bool {\n+        const SECTIONS: \u0026[\u0026str] = \u0026[\n+            \"api-type:\",\n+            \"param:\",\n+            \"error:\",\n+            \"capability:\",\n+            \"signal:\",\n+            \"lock:\",\n+            \"state-trans:\",\n+            \"constraint:\",\n+            \"side-effect:\",\n+            \"long-desc:\",\n+        ];\n+        let has_line = |keys: \u0026[\u0026str]| {\n+            doc.lines()\n+                .map(str::trim_start)\n+                .any(|l| keys.iter().any(|k| l.starts_with(k)))\n+        };\n+\n+        has_line(\u0026[\"contexts:\", \"context-flags:\"]) \u0026\u0026 has_line(SECTIONS)\n+    }\n+\n+    fn extract_from_content(\u0026self, content: \u0026str) -\u003e Result\u003cVec\u003cApiSpec\u003e\u003e {\n+        let mut specs = Vec::new();\n+        let mut in_kerneldoc = false;\n+        let mut current_doc = String::new();\n+        let lines: Vec\u003c\u0026str\u003e = content.lines().collect();\n+        let mut i = 0;\n+\n+        while i \u003c lines.len() {\n+            let line = lines[i];\n+\n+            // Start of kerneldoc comment\n+            if line.trim_start().starts_with(\"/**\") {\n+                in_kerneldoc = true;\n+                current_doc.clear();\n+                i += 1;\n+                continue;\n+            }\n+\n+            // Inside kerneldoc comment\n+            if in_kerneldoc {\n+                if line.contains(\"*/\") {\n+                    in_kerneldoc = false;\n+\n+                    // Check if this kerneldoc has KAPI annotations\n+                    if Self::has_context_line(\u0026current_doc) {\n+                        // Look ahead for the function declaration\n+                        if let Some((name, api_type, signature)) =\n+                            self.find_function_after(\u0026lines, i + 1)\n+                        {\n+                            if let Ok(spec) = self.parser.parse_kerneldoc(\n+                                \u0026current_doc,\n+                                \u0026name,\n+                                \u0026api_type,\n+                                Some(\u0026signature),\n+                            ) {\n+                                specs.push(spec);\n+                            }\n+                        }\n+                    }\n+                } else {\n+                    // Remove leading asterisk and preserve content\n+                    let cleaned = if let Some(stripped) = line.trim_start().strip_prefix(\"*\") {\n+                        if let Some(no_space) = stripped.strip_prefix(' ') {\n+                            no_space\n+                        } else {\n+                            stripped\n+                        }\n+                    } else {\n+                        line.trim_start()\n+                    };\n+                    current_doc.push_str(cleaned);\n+                    current_doc.push('\\n');\n+                }\n+            }\n+\n+            i += 1;\n+        }\n+\n+        Ok(specs)\n+    }\n+\n+    fn find_function_after(\n+        \u0026self,\n+        lines: \u0026[\u0026str],\n+        start: usize,\n+    ) -\u003e Option\u003c(String, String, String)\u003e {\n+        for i in start..lines.len().min(start + 10) {\n+            let line = lines[i];\n+\n+            // Skip blank lines and a plain comment before the function\n+            let trimmed = line.trim_start();\n+            if trimmed.is_empty() || trimmed.starts_with(\"/*\") || trimmed.starts_with('*') {\n+                continue;\n+            }\n+\n+            // Check for SYSCALL_DEFINE\n+            if let Some(caps) = self.syscall_regex.captures(line) {\n+                let name = format!(\"sys_{}\", caps.get(1).unwrap().as_str());\n+                let signature = self.extract_syscall_signature(lines, i);\n+                return Some((name, \"syscall\".to_string(), signature));\n+            }\n+\n+            // Check for ioctl function\n+            if let Some(caps) = self.ioctl_regex.captures(line) {\n+                let name = caps.get(1).unwrap().as_str().to_string();\n+                return Some((name, \"ioctl\".to_string(), line.to_string()));\n+            }\n+\n+            // Check for regular function\n+            if let Some(caps) = self.function_regex.captures(line) {\n+                let name = caps.get(1).unwrap().as_str().to_string();\n+                return Some((name, \"function\".to_string(), line.to_string()));\n+            }\n+\n+            // Stop if we hit something that's clearly not part of the function declaration\n+            if !line.starts_with(' ') \u0026\u0026 !line.starts_with('\\t') \u0026\u0026 !line.trim().is_empty() {\n+                break;\n+            }\n+        }\n+\n+        None\n+    }\n+\n+    fn extract_syscall_signature(\u0026self, lines: \u0026[\u0026str], start: usize) -\u003e String {\n+        // Extract the full SYSCALL_DEFINE signature\n+        let mut sig = String::new();\n+        let mut in_paren = false;\n+        let mut paren_count = 0;\n+\n+        for line in lines.iter().skip(start).take(20) {\n+            let line = *line;\n+\n+            // Start of SYSCALL_DEFINE\n+            if line.contains(\"SYSCALL_DEFINE\") {\n+                if let Some(pos) = line.find('(') {\n+                    sig.push_str(\u0026line[pos..]);\n+                    in_paren = true;\n+                    paren_count = line[pos..].chars().filter(|\u0026c| c == '(').count()\n+                        - line[pos..].chars().filter(|\u0026c| c == ')').count();\n+                }\n+            } else if in_paren {\n+                sig.push(' ');\n+                sig.push_str(line.trim());\n+                paren_count += line.chars().filter(|\u0026c| c == '(').count();\n+                paren_count =\n+                    paren_count.saturating_sub(line.chars().filter(|\u0026c| c == ')').count());\n+\n+                if paren_count == 0 {\n+                    break;\n+                }\n+            }\n+        }\n+\n+        sig\n+    }\n+}\n+\n+impl ApiExtractor for SourceExtractor {\n+    fn extract_all(\u0026self) -\u003e Result\u003cVec\u003cApiSpec\u003e\u003e {\n+        let path = Path::new(\u0026self.path);\n+        let mut all_specs = Vec::new();\n+\n+        if path.is_file() {\n+            // Single file\n+            all_specs.extend(self.extract_from_file(path)?);\n+        } else if path.is_dir() {\n+            // Directory - walk all .c files\n+            for entry in WalkDir::new(path)\n+                .into_iter()\n+                .filter_map(|e| e.ok())\n+                .filter(|e| {\n+                    e.path()\n+                        .extension()\n+                        .is_some_and(|ext| ext == \"c\" || ext == \"h\")\n+                })\n+            {\n+                match self.extract_from_file(entry.path()) {\n+                    Ok(specs) =\u003e all_specs.extend(specs),\n+                    Err(e) =\u003e {\n+                        eprintln!(\"Warning: failed to parse {}: {}\", entry.path().display(), e);\n+                    }\n+                }\n+            }\n+        }\n+\n+        Ok(all_specs)\n+    }\n+\n+    fn extract_by_name(\u0026self, name: \u0026str) -\u003e Result\u003cOption\u003cApiSpec\u003e\u003e {\n+        let all_specs = self.extract_all()?;\n+        Ok(all_specs.into_iter().find(|s| s.name == name))\n+    }\n+\n+    fn display_api_details(\n+        \u0026self,\n+        api_name: \u0026str,\n+        formatter: \u0026mut dyn OutputFormatter,\n+        output: \u0026mut dyn Write,\n+    ) -\u003e Result\u003c()\u003e {\n+        if let Some(spec) = self.extract_by_name(api_name)? {\n+            display_api_spec(\u0026spec, formatter, output)?;\n+        } else {\n+            writeln!(output, \"API '{}' not found\", api_name)?;\n+        }\n+        Ok(())\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+\n+    fn make_extractor() -\u003e SourceExtractor {\n+        SourceExtractor::new(\"/dev/null\").unwrap()\n+    }\n+\n+    #[test]\n+    fn detect_contexts_spec() {\n+        let content = r#\"\n+/**\n+ * sys_foo - do foo\n+ * @arg: the argument\n+ *\n+ * contexts: process, sleepable\n+ *\n+ * long-desc: Does foo.\n+ */\n+SYSCALL_DEFINE1(foo, int, arg)\n+{\n+    return 0;\n+}\n+\n+/**\n+ * bar - not a spec, even though the prose mentions context: here\n+ * @x: value\n+ */\n+SYSCALL_DEFINE1(bar, int, x)\n+{\n+    return 0;\n+}\n+\n+/**\n+ * baz - not a spec either\n+ * @x: value\n+ *\n+ * contexts: task, softirq, hardirq, nmi.\n+ */\n+SYSCALL_DEFINE1(baz, int, x)\n+{\n+    return 0;\n+}\n+\"#;\n+        let specs = make_extractor().extract_from_content(content).unwrap();\n+        assert_eq!(specs.len(), 1);\n+        assert_eq!(specs[0].name, \"sys_foo\");\n+    }\n+\n+    #[test]\n+    fn prose_mentioning_a_section_key_is_not_a_spec() {\n+        let content = r#\"\n+/**\n+ * __probe - check a device\n+ * @drv: driver\n+ *\n+ * returns 0 on success, else error.\n+ * side-effect: dev-\u003edriver is set to drv when drv claims dev.\n+ */\n+static int __probe(struct driver *drv, struct device *dev)\n+{\n+    return 0;\n+}\n+\"#;\n+        let specs = make_extractor().extract_from_content(content).unwrap();\n+        assert!(specs.is_empty());\n+    }\n+\n+    #[test]\n+    fn nonexistent_path_is_rejected() {\n+        let err = SourceExtractor::new(\"/nonexistent/kapi-test\")\n+            .err()\n+            .unwrap();\n+\n+        assert!(err.to_string().contains(\"does not exist\"), \"{err}\");\n+    }\n+\n+    #[test]\n+    fn has_context_line_needs_two_sections() {\n+        let rule = SourceExtractor::has_context_line;\n+\n+        assert!(rule(\"contexts: process\\nlong-desc: foo\\n\"));\n+        assert!(rule(\"context-flags: KAPI_CTX_PROCESS\\n  lock: foo\\n\"));\n+        assert!(rule(\"contexts: process\\nparam: fd\\n\"));\n+        assert!(rule(\"contexts: process\\nerror: EINVAL, bad fd\\n\"));\n+        assert!(!rule(\"contexts: process\\n\"));\n+        assert!(!rule(\"context: process\\nlong-desc: foo\\n\"));\n+        assert!(!rule(\"long-desc: foo\\nside-effect: bar\\n\"));\n+    }\n+\n+    #[test]\n+    fn detect_syscall_define3() {\n+        let content = r#\"\n+/**\n+ * sys_open - open a file\n+ * context-flags: KAPI_CTX_PROCESS\n+ * param-count: 3\n+ * @filename: pathname to open\n+ * param: filename\n+ * error: ENOENT, test\n+ */\n+SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)\n+{\n+    return 0;\n+}\n+\"#;\n+        let ext = make_extractor();\n+        let specs = ext.extract_from_content(content).unwrap();\n+        assert_eq!(specs.len(), 1);\n+        assert_eq!(specs[0].name, \"sys_open\");\n+        assert_eq!(specs[0].api_type, \"syscall\");\n+    }\n+\n+    #[test]\n+    fn detect_syscall_define1() {\n+        let content = r#\"\n+/**\n+ * sys_close - close a file descriptor\n+ * context-flags: KAPI_CTX_PROCESS\n+ * @fd: file descriptor to close\n+ * error: EBADF, test\n+ */\n+SYSCALL_DEFINE1(close, unsigned int, fd)\n+{\n+    return 0;\n+}\n+\"#;\n+        let ext = make_extractor();\n+        let specs = ext.extract_from_content(content).unwrap();\n+        assert_eq!(specs.len(), 1);\n+        assert_eq!(specs[0].name, \"sys_close\");\n+    }\n+\n+    #[test]\n+    fn detect_syscall_define6() {\n+        let content = r#\"\n+/**\n+ * sys_mmap - map memory\n+ * context-flags: KAPI_CTX_PROCESS\n+ * error: ENOMEM, test\n+ */\n+SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len, unsigned long, prot,\n+    unsigned long, flags, unsigned long, fd, unsigned long, offset)\n+{\n+    return 0;\n+}\n+\"#;\n+        let ext = make_extractor();\n+        let specs = ext.extract_from_content(content).unwrap();\n+        assert_eq!(specs.len(), 1);\n+        assert_eq!(specs[0].name, \"sys_mmap\");\n+    }\n+\n+    #[test]\n+    fn detect_ioctl_pattern() {\n+        let content = r#\"\n+/**\n+ * my_ioctl - handle ioctl\n+ * context-flags: KAPI_CTX_PROCESS\n+ * error: EINVAL, test\n+ */\n+static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)\n+{\n+    return 0;\n+}\n+\"#;\n+        let ext = make_extractor();\n+        let specs = ext.extract_from_content(content).unwrap();\n+        assert_eq!(specs.len(), 1);\n+        assert_eq!(specs[0].name, \"my_ioctl\");\n+        assert_eq!(specs[0].api_type, \"ioctl\");\n+    }\n+\n+    #[test]\n+    fn find_function_after_skips_blanks() {\n+        // Test that find_function_after looks past blank lines\n+        let lines = vec![\"\", \"\", \"SYSCALL_DEFINE2(foo, int, bar, int, baz)\", \"{\"];\n+        let ext = make_extractor();\n+        let result = ext.find_function_after(\u0026lines, 0);\n+        assert!(result.is_some());\n+        let (name, api_type, _sig) = result.unwrap();\n+        assert_eq!(name, \"sys_foo\");\n+        assert_eq!(api_type, \"syscall\");\n+    }\n+\n+    #[test]\n+    fn find_function_after_skips_plain_comment() {\n+        let lines = vec![\n+            \"/*\",\n+            \" * Careful here!\",\n+            \" */\",\n+            \"SYSCALL_DEFINE1(close, unsigned int, fd)\",\n+            \"{\",\n+        ];\n+        let ext = make_extractor();\n+        let (name, api_type, _sig) = ext.find_function_after(\u0026lines, 0).unwrap();\n+        assert_eq!(name, \"sys_close\");\n+        assert_eq!(api_type, \"syscall\");\n+    }\n+\n+    #[test]\n+    fn find_function_after_returns_none_for_no_match() {\n+        // No function declaration within lookahead range\n+        let lines = vec![\"#include \u003clinux/fs.h\u003e\", \"#define FOO 1\", \"/* comment */\"];\n+        let ext = make_extractor();\n+        let result = ext.find_function_after(\u0026lines, 0);\n+        assert!(result.is_none());\n+    }\n+\n+    #[test]\n+    fn find_function_after_detects_regular_function() {\n+        let lines = vec![\"\", \"int do_something(struct task_struct *task)\", \"{\"];\n+        let ext = make_extractor();\n+        let result = ext.find_function_after(\u0026lines, 0);\n+        assert!(result.is_some());\n+        let (name, api_type, _) = result.unwrap();\n+        assert_eq!(name, \"do_something\");\n+        assert_eq!(api_type, \"function\");\n+    }\n+\n+    #[test]\n+    fn no_kapi_annotations_produces_empty() {\n+        // kerneldoc without any KAPI annotations should not produce a spec\n+        let content = r#\"\n+/**\n+ * my_func - does stuff\n+ * @arg: an argument\n+ */\n+void my_func(int arg)\n+{\n+}\n+\"#;\n+        let ext = make_extractor();\n+        let specs = ext.extract_from_content(content).unwrap();\n+        assert!(specs.is_empty());\n+    }\n+\n+    #[test]\n+    fn multiple_syscalls_in_one_file() {\n+        let content = r#\"\n+/**\n+ * sys_read - read from fd\n+ * context-flags: KAPI_CTX_PROCESS\n+ * error: EBADF, test\n+ */\n+SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)\n+{\n+    return 0;\n+}\n+\n+/**\n+ * sys_write - write to fd\n+ * context-flags: KAPI_CTX_PROCESS\n+ * error: EBADF, test\n+ */\n+SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)\n+{\n+    return 0;\n+}\n+\"#;\n+        let ext = make_extractor();\n+        let specs = ext.extract_from_content(content).unwrap();\n+        assert_eq!(specs.len(), 2);\n+        assert_eq!(specs[0].name, \"sys_read\");\n+        assert_eq!(specs[1].name, \"sys_write\");\n+    }\n+}\ndiff --git a/tools/kapi/src/extractor/vmlinux/binary_utils.rs b/tools/kapi/src/extractor/vmlinux/binary_utils.rs\nnew file mode 100644\nindex 0000000000000..046772ea690c1\n--- /dev/null\n+++ b/tools/kapi/src/extractor/vmlinux/binary_utils.rs\n@@ -0,0 +1,461 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+// Array-bound constants matching `include/linux/kernel_api_spec.h`.\n+// String fields are `const char *`; call `DataReader::ptr_size()` for\n+// the per-target pointer width.\n+pub mod sizes {\n+    pub const MAX_PARAMS: usize = 16;\n+    pub const MAX_ERRORS: usize = 32;\n+    pub const MAX_CONSTRAINTS: usize = 32;\n+    pub const MAX_LOCKS: usize = 16;\n+    pub const MAX_CAPABILITIES: usize = 8;\n+    pub const MAX_SIGNALS: usize = 32;\n+    pub const MAX_STRUCT_SPECS: usize = 8;\n+    pub const MAX_SIDE_EFFECTS: usize = 32;\n+    pub const MAX_STATE_TRANS: usize = 8;\n+\n+    pub const NAME: usize = 0;\n+    pub const DESC: usize = 0;\n+}\n+\n+// Section markers of `struct kernel_api_spec`. The kernel only sets a\n+// marker when the matching *_COUNT()/KAPI_EXAMPLES() macro is used, so\n+// a zero marker is as valid as the expected one.\n+pub mod magic {\n+    pub const PARAMS: u32 = 0x4B415031; // 'KAP1'\n+    pub const RETURN: u32 = 0x4B415232; // 'KAR2'\n+    pub const ERRORS: u32 = 0x4B414533; // 'KAE3'\n+    pub const LOCKS: u32 = 0x4B414C34; // 'KAL4'\n+    pub const CONSTRAINTS: u32 = 0x4B414335; // 'KAC5'\n+    pub const INFO: u32 = 0x4B414936; // 'KAI6'\n+    pub const SIGNALS: u32 = 0x4B415337; // 'KAS7'\n+    pub const SIGMASK: u32 = 0x4B414D38; // 'KAM8'\n+    pub const STRUCTS: u32 = 0x4B415439; // 'KAT9'\n+    pub const EFFECTS: u32 = 0x4B414641; // 'KAFA'\n+    pub const TRANS: u32 = 0x4B415442; // 'KATB'\n+    pub const CAPS: u32 = 0x4B414343; // 'KACC'\n+}\n+\n+/// Resolve a virtual-address string pointer against the vmlinux ELF\n+/// and return the NUL-terminated C string it points at.\n+pub fn resolve_vaddr_string(elf: \u0026goblin::elf::Elf, data: \u0026[u8], vaddr: u64) -\u003e Option\u003cString\u003e {\n+    if vaddr == 0 {\n+        return None;\n+    }\n+    for sh in \u0026elf.section_headers {\n+        let start = sh.sh_addr;\n+        let end = start.checked_add(sh.sh_size)?;\n+        if vaddr \u003c start || vaddr \u003e= end {\n+            continue;\n+        }\n+        // File-backed sections only (skip SHT_NOBITS etc.)\n+        if sh.sh_type == goblin::elf::section_header::SHT_NOBITS {\n+            return None;\n+        }\n+        let rel = (vaddr - start) as usize;\n+        let file_start = sh.sh_offset as usize + rel;\n+        if file_start \u003e= data.len() {\n+            return None;\n+        }\n+        let tail = \u0026data[file_start..];\n+        let nul = tail.iter().position(|\u0026b| b == 0)?;\n+        return std::str::from_utf8(\u0026tail[..nul]).ok().map(str::to_string);\n+    }\n+    None\n+}\n+\n+/// Endianness of the target ELF binary\n+#[derive(Clone, Copy, PartialEq)]\n+pub enum Endian {\n+    Little,\n+    Big,\n+}\n+\n+/// Resolves string pointers read from `.kapi_specs` back to their\n+/// underlying C strings in the vmlinux rodata.\n+pub struct StringResolver\u003c'a\u003e {\n+    pub elf: \u0026'a goblin::elf::Elf\u003c'a\u003e,\n+    pub vmlinux: \u0026'a [u8],\n+}\n+\n+// Helper for reading data at specific offsets\n+pub struct DataReader\u003c'a\u003e {\n+    pub data: \u0026'a [u8],\n+    pub pos: usize,\n+    pub endian: Endian,\n+    /// true for 64-bit ELF, false for 32-bit\n+    pub is_64bit: bool,\n+    /// Used to follow `const char *` fields into rodata.\n+    pub resolver: Option\u003cStringResolver\u003c'a\u003e\u003e,\n+}\n+\n+impl\u003c'a\u003e DataReader\u003c'a\u003e {\n+    pub fn new(data: \u0026'a [u8], offset: usize, endian: Endian, is_64bit: bool) -\u003e Self {\n+        Self {\n+            data,\n+            pos: offset,\n+            endian,\n+            is_64bit,\n+            resolver: None,\n+        }\n+    }\n+\n+    pub fn with_resolver(mut self, resolver: StringResolver\u003c'a\u003e) -\u003e Self {\n+        self.resolver = Some(resolver);\n+        self\n+    }\n+\n+    /// Pointer width of the target in bytes (4 or 8).\n+    pub fn ptr_size(\u0026self) -\u003e usize {\n+        if self.is_64bit {\n+            8\n+        } else {\n+            4\n+        }\n+    }\n+\n+    /// Advance the read position to the next multiple of `align`.\n+    /// Needed before every naturally-aligned field when the containing\n+    /// struct is not `__packed`.\n+    pub fn align_to(\u0026mut self, align: usize) {\n+        if align \u003e 1 {\n+            let rem = self.pos % align;\n+            if rem != 0 {\n+                self.pos = (self.pos + (align - rem)).min(self.data.len());\n+            }\n+        }\n+    }\n+\n+    /// Read a target-sized pointer slot. Returns the virtual address\n+    /// stored in the slot, or `None` if there isn't enough data. The\n+    /// caller is expected to align the reader first if the containing\n+    /// struct demands natural alignment.\n+    pub fn read_ptr(\u0026mut self) -\u003e Option\u003cu64\u003e {\n+        self.align_to(self.ptr_size());\n+        if self.is_64bit {\n+            self.read_u64()\n+        } else {\n+            self.read_u32().map(|v| v as u64)\n+        }\n+    }\n+\n+    pub fn read_bytes(\u0026mut self, len: usize) -\u003e Option\u003c\u0026'a [u8]\u003e {\n+        if self.pos + len \u003c= self.data.len() {\n+            let bytes = \u0026self.data[self.pos..self.pos + len];\n+            self.pos += len;\n+            Some(bytes)\n+        } else {\n+            None\n+        }\n+    }\n+\n+    pub fn read_u32(\u0026mut self) -\u003e Option\u003cu32\u003e {\n+        self.align_to(4);\n+        let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();\n+        Some(match self.endian {\n+            Endian::Little =\u003e u32::from_le_bytes(b),\n+            Endian::Big =\u003e u32::from_be_bytes(b),\n+        })\n+    }\n+\n+    pub fn read_u8(\u0026mut self) -\u003e Option\u003cu8\u003e {\n+        self.read_bytes(1).map(|b| b[0])\n+    }\n+\n+    pub fn read_i32(\u0026mut self) -\u003e Option\u003ci32\u003e {\n+        self.align_to(4);\n+        let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();\n+        Some(match self.endian {\n+            Endian::Little =\u003e i32::from_le_bytes(b),\n+            Endian::Big =\u003e i32::from_be_bytes(b),\n+        })\n+    }\n+\n+    pub fn read_u64(\u0026mut self) -\u003e Option\u003cu64\u003e {\n+        self.align_to(8);\n+        let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();\n+        Some(match self.endian {\n+            Endian::Little =\u003e u64::from_le_bytes(b),\n+            Endian::Big =\u003e u64::from_be_bytes(b),\n+        })\n+    }\n+\n+    pub fn read_i64(\u0026mut self) -\u003e Option\u003ci64\u003e {\n+        self.align_to(8);\n+        let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();\n+        Some(match self.endian {\n+            Endian::Little =\u003e i64::from_le_bytes(b),\n+            Endian::Big =\u003e i64::from_be_bytes(b),\n+        })\n+    }\n+\n+    /// Read a target-sized unsigned value (4 bytes for 32-bit, 8 bytes for 64-bit).\n+    pub fn read_usize(\u0026mut self) -\u003e Option\u003cusize\u003e {\n+        self.align_to(self.ptr_size());\n+        if self.is_64bit {\n+            // No double-align: read_u64 would re-align, but we just\n+            // did that with ptr_size() which is 8 on 64-bit.\n+            let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();\n+            Some(match self.endian {\n+                Endian::Little =\u003e u64::from_le_bytes(b) as usize,\n+                Endian::Big =\u003e u64::from_be_bytes(b) as usize,\n+            })\n+        } else {\n+            let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();\n+            Some(match self.endian {\n+                Endian::Little =\u003e u32::from_le_bytes(b) as usize,\n+                Endian::Big =\u003e u32::from_be_bytes(b) as usize,\n+            })\n+        }\n+    }\n+\n+    // Helper methods for common patterns\n+    pub fn read_bool(\u0026mut self) -\u003e Option\u003cbool\u003e {\n+        self.read_u8().map(|v| v != 0)\n+    }\n+\n+    /// Read a `const char *` slot using the target pointer width\n+    /// (4 bytes on 32-bit, 8 bytes on 64-bit) and, if a resolver is\n+    /// attached, follow the address into the vmlinux to recover the\n+    /// C string. The `_max_len` argument is ignored.\n+    pub fn read_optional_string(\u0026mut self, _max_len: usize) -\u003e Option\u003cString\u003e {\n+        let vaddr = self.read_ptr()?;\n+        let resolver = self.resolver.as_ref()?;\n+        resolve_vaddr_string(resolver.elf, resolver.vmlinux, vaddr).filter(|s| !s.is_empty())\n+    }\n+\n+    pub fn read_string_or_default(\u0026mut self, max_len: usize) -\u003e String {\n+        self.read_optional_string(max_len).unwrap_or_default()\n+    }\n+\n+    /// Follow a `const s64 *` slot (already read with `read_ptr`) to the\n+    /// `count` values it points at. A NULL or unresolvable pointer yields\n+    /// an empty list.\n+    pub fn resolve_s64_array(\u0026self, vaddr: u64, count: u32) -\u003e Vec\u003ci64\u003e {\n+        let Some(resolver) = self.resolver.as_ref() else {\n+            return Vec::new();\n+        };\n+        if vaddr == 0 {\n+            return Vec::new();\n+        }\n+        let Some(start) = super::vaddr_to_file_offset(resolver.elf, vaddr) else {\n+            return Vec::new();\n+        };\n+        let raw = (count as usize)\n+            .checked_mul(8)\n+            .and_then(|len| start.checked_add(len))\n+            .and_then(|end| resolver.vmlinux.get(start..end));\n+        raw.map_or_else(Vec::new, |raw| {\n+            raw.chunks_exact(8)\n+                .map(|chunk| {\n+                    let b: [u8; 8] = chunk.try_into().unwrap();\n+                    match self.endian {\n+                        Endian::Little =\u003e i64::from_le_bytes(b),\n+                        Endian::Big =\u003e i64::from_be_bytes(b),\n+                    }\n+                })\n+                .collect()\n+        })\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+\n+    // ---- DataReader little-endian tests ----\n+\n+    #[test]\n+    fn read_u32_little_endian() {\n+        let data = [0x78, 0x56, 0x34, 0x12];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_u32(), Some(0x12345678));\n+    }\n+\n+    #[test]\n+    fn read_u32_big_endian() {\n+        let data = [0x12, 0x34, 0x56, 0x78];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Big, true);\n+        assert_eq!(reader.read_u32(), Some(0x12345678));\n+    }\n+\n+    #[test]\n+    fn read_u64_little_endian() {\n+        let data = 0xDEADBEEFCAFEBABEu64.to_le_bytes();\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_u64(), Some(0xDEADBEEFCAFEBABE));\n+    }\n+\n+    #[test]\n+    fn read_u64_big_endian() {\n+        let data = 0xDEADBEEFCAFEBABEu64.to_be_bytes();\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Big, true);\n+        assert_eq!(reader.read_u64(), Some(0xDEADBEEFCAFEBABE));\n+    }\n+\n+    #[test]\n+    fn read_i32_little_endian_negative() {\n+        let val: i32 = -42;\n+        let data = val.to_le_bytes();\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_i32(), Some(-42));\n+    }\n+\n+    #[test]\n+    fn read_i32_big_endian_negative() {\n+        let val: i32 = -1;\n+        let data = val.to_be_bytes();\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Big, true);\n+        assert_eq!(reader.read_i32(), Some(-1));\n+    }\n+\n+    #[test]\n+    fn read_i64_little_endian() {\n+        let val: i64 = -9999999999;\n+        let data = val.to_le_bytes();\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_i64(), Some(-9999999999));\n+    }\n+\n+    #[test]\n+    fn read_i64_big_endian() {\n+        let val: i64 = i64::MIN;\n+        let data = val.to_be_bytes();\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Big, true);\n+        assert_eq!(reader.read_i64(), Some(i64::MIN));\n+    }\n+\n+    // ---- read_usize tests ----\n+\n+    #[test]\n+    fn read_usize_64bit() {\n+        let val: u64 = 0x00000000FFFFFFFF;\n+        let data = val.to_le_bytes();\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_usize(), Some(0xFFFFFFFF));\n+    }\n+\n+    #[test]\n+    fn read_usize_32bit() {\n+        let val: u32 = 0xABCD1234;\n+        let data = val.to_le_bytes();\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, false);\n+        assert_eq!(reader.read_usize(), Some(0xABCD1234));\n+    }\n+\n+    #[test]\n+    fn read_usize_32bit_does_not_consume_8_bytes() {\n+        // In 32-bit mode, read_usize should only consume 4 bytes\n+        let mut data = [0u8; 8];\n+        data[..4].copy_from_slice(\u002642u32.to_le_bytes());\n+        data[4..8].copy_from_slice(\u002699u32.to_le_bytes());\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, false);\n+        assert_eq!(reader.read_usize(), Some(42));\n+        // After reading 4 bytes, pos should be at 4\n+        assert_eq!(reader.pos, 4);\n+        assert_eq!(reader.read_usize(), Some(99));\n+    }\n+\n+    // ---- Bounds checking ----\n+\n+    #[test]\n+    fn read_u32_past_end_returns_none() {\n+        let data = [0x01, 0x02, 0x03]; // only 3 bytes, need 4\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_u32(), None);\n+    }\n+\n+    #[test]\n+    fn read_u64_past_end_returns_none() {\n+        let data = [0u8; 7]; // only 7 bytes, need 8\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_u64(), None);\n+    }\n+\n+    #[test]\n+    fn read_bytes_past_end_returns_none() {\n+        let data = [0u8; 4];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_bytes(5), None);\n+    }\n+\n+    #[test]\n+    fn read_at_offset() {\n+        // read_u32 auto-aligns to a 4-byte boundary, so the starting\n+        // offset must itself be 4-aligned for the value to be read\n+        // from its declared position.\n+        let data = [0xFF, 0xFF, 0xFF, 0xFF, 0x78, 0x56, 0x34, 0x12];\n+        let mut reader = DataReader::new(\u0026data, 4, Endian::Little, true);\n+        assert_eq!(reader.read_u32(), Some(0x12345678));\n+    }\n+\n+    #[test]\n+    fn read_u32_auto_aligns() {\n+        // Starting mid-word, read_u32 snaps to the next 4-byte boundary.\n+        let data = [0xDE, 0xAD, 0xBE, 0xEF, 0x78, 0x56, 0x34, 0x12];\n+        let mut reader = DataReader::new(\u0026data, 1, Endian::Little, true);\n+        assert_eq!(reader.read_u32(), Some(0x12345678));\n+        assert_eq!(reader.pos, 8);\n+    }\n+\n+    #[test]\n+    fn read_ptr_32bit_uses_4_bytes() {\n+        let data = [0x78, 0x56, 0x34, 0x12];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, false);\n+        assert_eq!(reader.read_ptr(), Some(0x12345678));\n+        assert_eq!(reader.pos, 4);\n+    }\n+\n+    #[test]\n+    fn read_ptr_64bit_uses_8_bytes() {\n+        let data = [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_ptr(), Some(0x12345678));\n+        assert_eq!(reader.pos, 8);\n+    }\n+\n+    #[test]\n+    fn read_bool_values() {\n+        let data = [0, 1, 255];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_bool(), Some(false));\n+        assert_eq!(reader.read_bool(), Some(true));\n+        assert_eq!(reader.read_bool(), Some(true)); // any non-zero is true\n+    }\n+\n+    #[test]\n+    fn sequential_reads_advance_position() {\n+        let mut data = [0u8; 12];\n+        data[..4].copy_from_slice(\u00261u32.to_le_bytes());\n+        data[4..8].copy_from_slice(\u00262u32.to_le_bytes());\n+        data[8..12].copy_from_slice(\u00263u32.to_le_bytes());\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_u32(), Some(1));\n+        assert_eq!(reader.read_u32(), Some(2));\n+        assert_eq!(reader.read_u32(), Some(3));\n+        assert_eq!(reader.pos, 12);\n+    }\n+\n+    #[test]\n+    fn read_optional_string_empty_returns_none() {\n+        // A string buffer that is just NUL\n+        let data = [0u8; 10];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_optional_string(10), None);\n+    }\n+\n+    #[test]\n+    fn read_string_or_default_with_empty() {\n+        let data = [0u8; 10];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_string_or_default(10), \"\");\n+    }\n+\n+    #[test]\n+    fn read_u8_value() {\n+        let data = [0x42];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+        assert_eq!(reader.read_u8(), Some(0x42));\n+    }\n+}\ndiff --git a/tools/kapi/src/extractor/vmlinux/mod.rs b/tools/kapi/src/extractor/vmlinux/mod.rs\nnew file mode 100644\nindex 0000000000000..5202f7d80c8d8\n--- /dev/null\n+++ b/tools/kapi/src/extractor/vmlinux/mod.rs\n@@ -0,0 +1,1160 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use super::{\n+    ApiExtractor, ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec,\n+    ReturnSpec, SideEffectSpec, SignalMaskSpec, SignalSpec, StateTransitionSpec, StructFieldSpec,\n+    StructSpec,\n+};\n+use crate::formatter::OutputFormatter;\n+use anyhow::{Context, Result};\n+use goblin::elf::Elf;\n+use std::fs;\n+use std::io::Write;\n+\n+mod binary_utils;\n+use binary_utils::{magic, sizes, DataReader, Endian};\n+\n+// Helper to convert empty strings to None\n+fn opt_string(s: String) -\u003e Option\u003cString\u003e {\n+    if s.is_empty() {\n+        None\n+    } else {\n+        Some(s)\n+    }\n+}\n+\n+pub struct VmlinuxExtractor {\n+    vmlinux: Vec\u003cu8\u003e,\n+    specs: Vec\u003cKapiSpec\u003e,\n+    endian: Endian,\n+    is_64bit: bool,\n+}\n+\n+#[derive(Debug)]\n+struct KapiSpec {\n+    name: String,\n+    api_type: String,\n+    /// File offset in the vmlinux buffer where this spec's\n+    /// `struct kernel_api_spec` begins.\n+    file_offset: usize,\n+}\n+\n+impl VmlinuxExtractor {\n+    pub fn new(vmlinux_path: \u0026str) -\u003e Result\u003cSelf\u003e {\n+        let vmlinux = fs::read(vmlinux_path)\n+            .with_context(|| format!(\"Failed to read vmlinux file: {vmlinux_path}\"))?;\n+\n+        let elf = Elf::parse(\u0026vmlinux).context(\"Failed to parse ELF file\")?;\n+        let endian = if elf.little_endian {\n+            Endian::Little\n+        } else {\n+            Endian::Big\n+        };\n+        let is_64bit = elf.is_64;\n+\n+        // Locate the .kapi_specs section boundaries.\n+        let mut start_addr = None;\n+        let mut stop_addr = None;\n+        for sym in \u0026elf.syms {\n+            if let Some(name) = elf.strtab.get_at(sym.st_name) {\n+                match name {\n+                    \"__start_kapi_specs\" =\u003e start_addr = Some(sym.st_value),\n+                    \"__stop_kapi_specs\" =\u003e stop_addr = Some(sym.st_value),\n+                    _ =\u003e {}\n+                }\n+            }\n+        }\n+        let start = start_addr.context(\"Could not find __start_kapi_specs symbol\")?;\n+        let stop = stop_addr.context(\"Could not find __stop_kapi_specs symbol\")?;\n+        if stop \u003c= start {\n+            anyhow::bail!(\"No kernel API specifications found in vmlinux\");\n+        }\n+\n+        // `.kapi_specs` is a tightly-packed array of `struct kernel_api_spec *`\n+        // pointers; walk them to find each real spec's vaddr, then resolve to\n+        // a file offset inside `vmlinux`. Pointer width tracks the target\n+        // (4 bytes for 32-bit, 8 bytes for 64-bit).\n+        let ptr_size = if is_64bit { 8usize } else { 4 };\n+        let ptr_count = ((stop - start) as usize) / ptr_size;\n+        let ptr_file_off =\n+            vaddr_to_file_offset(\u0026elf, start).context(\"Could not locate .kapi_specs in file\")?;\n+\n+        let read_ptr = |raw: \u0026[u8]| -\u003e u64 {\n+            match (endian, is_64bit) {\n+                (Endian::Little, true) =\u003e u64::from_le_bytes(raw.try_into().unwrap()),\n+                (Endian::Big, true) =\u003e u64::from_be_bytes(raw.try_into().unwrap()),\n+                (Endian::Little, false) =\u003e u32::from_le_bytes(raw.try_into().unwrap()) as u64,\n+                (Endian::Big, false) =\u003e u32::from_be_bytes(raw.try_into().unwrap()) as u64,\n+            }\n+        };\n+\n+        let mut specs = Vec::with_capacity(ptr_count);\n+        for i in 0..ptr_count {\n+            let p = ptr_file_off + i * ptr_size;\n+            if p + ptr_size \u003e vmlinux.len() {\n+                break;\n+            }\n+            let spec_vaddr = read_ptr(\u0026vmlinux[p..p + ptr_size]);\n+            if spec_vaddr == 0 {\n+                continue;\n+            }\n+            let Some(spec_file_off) = vaddr_to_file_offset(\u0026elf, spec_vaddr) else {\n+                continue;\n+            };\n+            // The first field of `struct kernel_api_spec` is `const char *name`.\n+            if spec_file_off + ptr_size \u003e vmlinux.len() {\n+                continue;\n+            }\n+            let name_vaddr = read_ptr(\u0026vmlinux[spec_file_off..spec_file_off + ptr_size]);\n+            let name =\n+                binary_utils::resolve_vaddr_string(\u0026elf, \u0026vmlinux, name_vaddr).unwrap_or_default();\n+            if name.is_empty() {\n+                continue;\n+            }\n+            let api_type = if name.starts_with(\"sys_\") {\n+                \"syscall\"\n+            } else if name.ends_with(\"_ioctl\") {\n+                \"ioctl\"\n+            } else {\n+                \"function\"\n+            }\n+            .to_string();\n+            specs.push(KapiSpec {\n+                name,\n+                api_type,\n+                file_offset: spec_file_off,\n+            });\n+        }\n+\n+        Ok(VmlinuxExtractor {\n+            vmlinux,\n+            specs,\n+            endian,\n+            is_64bit,\n+        })\n+    }\n+}\n+\n+/// Map a virtual address to a file offset inside the raw vmlinux bytes.\n+fn vaddr_to_file_offset(elf: \u0026Elf, vaddr: u64) -\u003e Option\u003cusize\u003e {\n+    for sh in \u0026elf.section_headers {\n+        let start = sh.sh_addr;\n+        let end = start.checked_add(sh.sh_size)?;\n+        if vaddr \u003e= start \u0026\u0026 vaddr \u003c end {\n+            if sh.sh_type == goblin::elf::section_header::SHT_NOBITS {\n+                return None;\n+            }\n+            return Some((sh.sh_offset + (vaddr - start)) as usize);\n+        }\n+    }\n+    None\n+}\n+\n+impl VmlinuxExtractor {\n+    fn parse_at(\u0026self, file_offset: usize) -\u003e Result\u003cApiSpec\u003e {\n+        parse_binary_to_api_spec(\u0026self.vmlinux, file_offset, self.endian, self.is_64bit)\n+    }\n+}\n+\n+impl ApiExtractor for VmlinuxExtractor {\n+    fn extract_all(\u0026self) -\u003e Result\u003cVec\u003cApiSpec\u003e\u003e {\n+        Ok(self\n+            .specs\n+            .iter()\n+            .map(|spec| {\n+                self.parse_at(spec.file_offset).unwrap_or_else(|e| {\n+                    eprintln!(\"Warning: cannot parse the spec of {}: {e:#}\", spec.name);\n+                    ApiSpec {\n+                        name: spec.name.clone(),\n+                        api_type: spec.api_type.clone(),\n+                        ..Default::default()\n+                    }\n+                })\n+            })\n+            .collect())\n+    }\n+\n+    fn extract_by_name(\u0026self, api_name: \u0026str) -\u003e Result\u003cOption\u003cApiSpec\u003e\u003e {\n+        if let Some(spec) = self.specs.iter().find(|s| s.name == api_name) {\n+            Ok(Some(self.parse_at(spec.file_offset)?))\n+        } else {\n+            Ok(None)\n+        }\n+    }\n+\n+    fn display_api_details(\n+        \u0026self,\n+        api_name: \u0026str,\n+        formatter: \u0026mut dyn OutputFormatter,\n+        writer: \u0026mut dyn Write,\n+    ) -\u003e Result\u003c()\u003e {\n+        if let Some(spec) = self.specs.iter().find(|s| s.name == api_name) {\n+            let api_spec = self.parse_at(spec.file_offset)?;\n+            super::display_api_spec(\u0026api_spec, formatter, writer)?;\n+        }\n+        Ok(())\n+    }\n+}\n+\n+const TRUNCATED: \u0026str = \"kernel_api_spec runs past the end of the file\";\n+\n+/// Consume a section marker. The kernel leaves it zero when the matching\n+/// macro is not used, so only a different non-zero value means that the\n+/// reader has lost sync with the struct layout.\n+fn read_magic(reader: \u0026mut DataReader, expected: u32) -\u003e Result\u003c()\u003e {\n+    let found = reader.read_u32().context(TRUNCATED)?;\n+    if found != 0 \u0026\u0026 found != expected {\n+        anyhow::bail!(\n+            \"unexpected section marker {found:#x} at offset {:#x}, expected {expected:#x}\",\n+            reader.pos - 4\n+        );\n+    }\n+    Ok(())\n+}\n+\n+/// Parse a `{ u32 magic; u32 count; T items[MAX]; }` section. Every one of\n+/// the `max_items` slots is consumed, so the reader ends up behind the\n+/// whole array; only the first `count` are returned.\n+fn parse_array\u003cT, F\u003e(\n+    reader: \u0026mut DataReader,\n+    expected_magic: u32,\n+    max_items: usize,\n+    parse_fn: F,\n+) -\u003e Result\u003cVec\u003cT\u003e\u003e\n+where\n+    F: Fn(\u0026mut DataReader, usize) -\u003e Option\u003cT\u003e,\n+{\n+    read_magic(reader, expected_magic)?;\n+    let count = reader.read_u32().context(TRUNCATED)? as usize;\n+    let align = reader.ptr_size();\n+    reader.align_to(align);\n+\n+    let mut items = Vec::new();\n+    for i in 0..max_items {\n+        let item = parse_fn(reader, i).context(TRUNCATED)?;\n+        reader.align_to(align);\n+        if i \u003c count {\n+            items.push(item);\n+        }\n+    }\n+    Ok(items)\n+}\n+\n+fn parse_binary_to_api_spec(\n+    data: \u0026[u8],\n+    offset: usize,\n+    endian: Endian,\n+    is_64bit: bool,\n+) -\u003e Result\u003cApiSpec\u003e {\n+    let elf = Elf::parse(data).context(\"Failed to re-parse ELF for string resolution\")?;\n+    let resolver = binary_utils::StringResolver {\n+        elf: \u0026elf,\n+        vmlinux: data,\n+    };\n+    let mut reader = DataReader::new(data, offset, endian, is_64bit).with_resolver(resolver);\n+\n+    // Read fields in exact order of struct kernel_api_spec.\n+    // Every string field is a `const char *` pointer resolved via the\n+    // StringResolver attached to the DataReader.\n+    let name = reader\n+        .read_optional_string(sizes::NAME)\n+        .ok_or_else(|| anyhow::anyhow!(\"Failed to read API name\"))?;\n+\n+    // Determine API type\n+    let api_type = if name.starts_with(\"sys_\") {\n+        \"syscall\"\n+    } else if name.ends_with(\"_ioctl\") {\n+        \"ioctl\"\n+    } else if name.contains(\"sysfs\") {\n+        \"sysfs\"\n+    } else {\n+        \"function\"\n+    }\n+    .to_string();\n+\n+    let version = reader.read_u32().map(|v| v.to_string());\n+\n+    let description = reader\n+        .read_optional_string(sizes::DESC)\n+        .filter(|s| !s.is_empty());\n+\n+    let long_description = reader\n+        .read_optional_string(sizes::DESC)\n+        .filter(|s| !s.is_empty());\n+\n+    let context_flags = parse_context_flags(\u0026mut reader);\n+\n+    let parameters = parse_array(\u0026mut reader, magic::PARAMS, sizes::MAX_PARAMS, parse_param)?;\n+\n+    read_magic(\u0026mut reader, magic::RETURN)?;\n+    let return_spec = parse_return_spec(\u0026mut reader);\n+\n+    let errors = parse_array(\u0026mut reader, magic::ERRORS, sizes::MAX_ERRORS, |r, _| {\n+        parse_error(r)\n+    })?;\n+\n+    let locks = parse_array(\u0026mut reader, magic::LOCKS, sizes::MAX_LOCKS, |r, _| {\n+        parse_lock(r)\n+    })?;\n+\n+    let constraints = parse_array(\n+        \u0026mut reader,\n+        magic::CONSTRAINTS,\n+        sizes::MAX_CONSTRAINTS,\n+        |r, _| parse_constraint(r),\n+    )?;\n+\n+    // Only KAPI_EXAMPLES() sets info_magic, so it says nothing about\n+    // whether notes are present.\n+    read_magic(\u0026mut reader, magic::INFO)?;\n+    let examples = reader\n+        .read_optional_string(sizes::DESC)\n+        .filter(|s| !s.is_empty());\n+    let notes = reader\n+        .read_optional_string(sizes::DESC)\n+        .filter(|s| !s.is_empty());\n+\n+    let signals = parse_array(\u0026mut reader, magic::SIGNALS, sizes::MAX_SIGNALS, |r, _| {\n+        parse_signal(r)\n+    })?;\n+\n+    let signal_masks = parse_array(\u0026mut reader, magic::SIGMASK, sizes::MAX_SIGNALS, |r, _| {\n+        parse_signal_mask(r)\n+    })?;\n+\n+    let struct_specs = parse_array(\n+        \u0026mut reader,\n+        magic::STRUCTS,\n+        sizes::MAX_STRUCT_SPECS,\n+        |r, _| parse_struct_spec(r),\n+    )?;\n+\n+    let side_effects = parse_array(\n+        \u0026mut reader,\n+        magic::EFFECTS,\n+        sizes::MAX_SIDE_EFFECTS,\n+        |r, _| parse_side_effect(r),\n+    )?;\n+\n+    let state_transitions =\n+        parse_array(\u0026mut reader, magic::TRANS, sizes::MAX_STATE_TRANS, |r, _| {\n+            parse_state_transition(r)\n+        })?;\n+\n+    let capabilities = parse_array(\u0026mut reader, magic::CAPS, sizes::MAX_CAPABILITIES, |r, _| {\n+        parse_capability(r)\n+    })?;\n+\n+    Ok(ApiSpec {\n+        name,\n+        api_type,\n+        description,\n+        long_description,\n+        version,\n+        context_flags,\n+        param_count: if parameters.is_empty() {\n+            None\n+        } else {\n+            Some(parameters.len() as u32)\n+        },\n+        error_count: if errors.is_empty() {\n+            None\n+        } else {\n+            Some(errors.len() as u32)\n+        },\n+        examples,\n+        notes,\n+        subsystem: None,\n+        sysfs_path: None,\n+        permissions: None,\n+        capabilities,\n+        parameters,\n+        return_spec,\n+        errors,\n+        signals,\n+        signal_masks,\n+        side_effects,\n+        state_transitions,\n+        constraints,\n+        locks,\n+        struct_specs,\n+    })\n+}\n+\n+// Helper parsing functions\n+\n+fn parse_context_flags(reader: \u0026mut DataReader) -\u003e Vec\u003cString\u003e {\n+    const KAPI_CTX_PROCESS: u32 = 1 \u003c\u003c 0;\n+    const KAPI_CTX_SOFTIRQ: u32 = 1 \u003c\u003c 1;\n+    const KAPI_CTX_HARDIRQ: u32 = 1 \u003c\u003c 2;\n+    const KAPI_CTX_NMI: u32 = 1 \u003c\u003c 3;\n+    const KAPI_CTX_ATOMIC: u32 = 1 \u003c\u003c 4;\n+    const KAPI_CTX_SLEEPABLE: u32 = 1 \u003c\u003c 5;\n+    const KAPI_CTX_PREEMPT_DISABLED: u32 = 1 \u003c\u003c 6;\n+    const KAPI_CTX_IRQ_DISABLED: u32 = 1 \u003c\u003c 7;\n+\n+    if let Some(flags) = reader.read_u32() {\n+        let mut parts = Vec::new();\n+\n+        if flags \u0026 KAPI_CTX_PROCESS != 0 {\n+            parts.push(\"KAPI_CTX_PROCESS\");\n+        }\n+        if flags \u0026 KAPI_CTX_SOFTIRQ != 0 {\n+            parts.push(\"KAPI_CTX_SOFTIRQ\");\n+        }\n+        if flags \u0026 KAPI_CTX_HARDIRQ != 0 {\n+            parts.push(\"KAPI_CTX_HARDIRQ\");\n+        }\n+        if flags \u0026 KAPI_CTX_NMI != 0 {\n+            parts.push(\"KAPI_CTX_NMI\");\n+        }\n+        if flags \u0026 KAPI_CTX_ATOMIC != 0 {\n+            parts.push(\"KAPI_CTX_ATOMIC\");\n+        }\n+        if flags \u0026 KAPI_CTX_SLEEPABLE != 0 {\n+            parts.push(\"KAPI_CTX_SLEEPABLE\");\n+        }\n+        if flags \u0026 KAPI_CTX_PREEMPT_DISABLED != 0 {\n+            parts.push(\"KAPI_CTX_PREEMPT_DISABLED\");\n+        }\n+        if flags \u0026 KAPI_CTX_IRQ_DISABLED != 0 {\n+            parts.push(\"KAPI_CTX_IRQ_DISABLED\");\n+        }\n+\n+        parts.into_iter().map(|s| s.to_string()).collect()\n+    } else {\n+        vec![]\n+    }\n+}\n+\n+fn parse_param(reader: \u0026mut DataReader, index: usize) -\u003e Option\u003cParamSpec\u003e {\n+    let name = reader.read_string_or_default(sizes::NAME);\n+    let type_name = reader.read_string_or_default(sizes::NAME);\n+    let param_type = reader.read_u32()?;\n+    let flags = reader.read_u32()?;\n+    let size = reader.read_usize()?;\n+    let alignment = reader.read_usize()?;\n+    let min_value = reader.read_i64()?;\n+    let max_value = reader.read_i64()?;\n+    let valid_mask = reader.read_u64()?;\n+\n+    let enum_values_ptr = reader.read_ptr()?;\n+    let enum_count = reader.read_u32()?;\n+    let constraint_type = reader.read_u32()?;\n+    // Skip validate function pointer\n+    reader.read_ptr()?;\n+\n+    let description = reader.read_string_or_default(sizes::DESC);\n+    let constraint = reader.read_optional_string(sizes::DESC);\n+    let size_param_idx_raw = reader.read_i32()?; // Must use ? to propagate errors\n+    let _size_multiplier = reader.read_usize()?; // Must use ? to propagate errors\n+\n+    // In the C struct, size_param_idx is stored 1-based; 0 means\n+    // \"no size-carrying param\". Surface the real (0-based) index as\n+    // `Option\u003cu32\u003e`.\n+    let size_param_idx = if size_param_idx_raw \u003e 0 {\n+        Some((size_param_idx_raw - 1) as u32)\n+    } else {\n+        None\n+    };\n+\n+    let mut param = ParamSpec {\n+        index: index as u32,\n+        name,\n+        type_name,\n+        description,\n+        flags,\n+        param_type,\n+        constraint_type,\n+        constraint,\n+        min_value: Some(min_value),\n+        max_value: Some(max_value),\n+        valid_mask: Some(valid_mask),\n+        enum_values: reader\n+            .resolve_s64_array(enum_values_ptr, enum_count)\n+            .iter()\n+            .map(i64::to_string)\n+            .collect(),\n+        size: Some(size as u32),\n+        alignment: Some(alignment as u32),\n+        size_param_idx,\n+    };\n+    param.keep_used_numbers();\n+    Some(param)\n+}\n+\n+fn parse_return_spec(reader: \u0026mut DataReader) -\u003e Option\u003cReturnSpec\u003e {\n+    // Read type_name, but treat empty as valid (will be empty string)\n+    let type_name = reader.read_string_or_default(sizes::NAME);\n+\n+    // Read return_type and check_type\n+    let return_type = reader.read_u32().unwrap_or(0);\n+    let check_type = reader.read_u32().unwrap_or(0);\n+    let success_value = reader.read_i64().unwrap_or(0);\n+    let success_min = reader.read_i64().unwrap_or(0);\n+    let success_max = reader.read_i64().unwrap_or(0);\n+\n+    let error_values_ptr = reader.read_ptr().unwrap_or(0);\n+    let error_count = reader.read_u32().unwrap_or(0);\n+    // Skip is_success function pointer\n+    let _ = reader.read_ptr();\n+\n+    let description = reader.read_string_or_default(sizes::DESC);\n+\n+    // Return a spec even if type_name is empty, as long as we have some data\n+    // The type_name might be a string like \"KAPI_TYPE_INT\" that gets stored literally\n+    if type_name.is_empty() \u0026\u0026 return_type == 0 \u0026\u0026 check_type == 0 \u0026\u0026 success_value == 0 {\n+        // No return spec at all\n+        return None;\n+    }\n+\n+    let mut ret = ReturnSpec {\n+        type_name,\n+        description,\n+        return_type,\n+        check_type,\n+        success_value: Some(success_value),\n+        success_min: Some(success_min),\n+        success_max: Some(success_max),\n+        error_values: reader\n+            .resolve_s64_array(error_values_ptr, error_count)\n+            .into_iter()\n+            .filter_map(|v| i32::try_from(v).ok())\n+            .collect(),\n+    };\n+    ret.keep_used_success_fields();\n+    Some(ret)\n+}\n+\n+fn parse_error(reader: \u0026mut DataReader) -\u003e Option\u003cErrorSpec\u003e {\n+    let error_code = reader.read_i32()?;\n+    let name = reader.read_string_or_default(sizes::NAME);\n+    let condition = reader.read_string_or_default(sizes::DESC);\n+    let description = reader.read_string_or_default(sizes::DESC);\n+\n+    Some(ErrorSpec {\n+        error_code,\n+        name,\n+        condition,\n+        description,\n+    })\n+}\n+\n+fn parse_lock(reader: \u0026mut DataReader) -\u003e Option\u003cLockSpec\u003e {\n+    let lock_name = reader.read_string_or_default(sizes::NAME);\n+    let lock_type = reader.read_u32()?;\n+    let scope = reader.read_u32()?;\n+    let description = reader.read_string_or_default(sizes::DESC);\n+\n+    Some(LockSpec {\n+        lock_name,\n+        lock_type,\n+        scope,\n+        description,\n+    })\n+}\n+\n+fn parse_constraint(reader: \u0026mut DataReader) -\u003e Option\u003cConstraintSpec\u003e {\n+    let name = reader.read_string_or_default(sizes::NAME);\n+    let description = reader.read_string_or_default(sizes::DESC);\n+    let expression = reader.read_string_or_default(sizes::DESC);\n+\n+    Some(ConstraintSpec {\n+        name,\n+        description,\n+        expression: opt_string(expression),\n+    })\n+}\n+\n+fn parse_signal(reader: \u0026mut DataReader) -\u003e Option\u003cSignalSpec\u003e {\n+    // Matches `struct kapi_signal_spec`. All string fields are pointers.\n+    let signal_num = reader.read_i32()?;\n+    let signal_name = reader.read_optional_string(sizes::NAME).unwrap_or_default();\n+    let direction = reader.read_u32()?;\n+    let action = reader.read_u32()?;\n+    let target = reader.read_optional_string(sizes::DESC);\n+    let condition = reader.read_optional_string(sizes::DESC);\n+    let description = reader.read_optional_string(sizes::DESC);\n+    let restartable = reader.read_bool()?;\n+    let sa_flags_required = reader.read_u32()?;\n+    let sa_flags_forbidden = reader.read_u32()?;\n+    let error_on_signal = reader.read_i32()?;\n+    let transform_to = reader.read_i32()?;\n+    // Read the symbolic timing token (const char *) and map it to the\n+    // numeric timing code used by downstream consumers.\n+    let timing_str = reader.read_optional_string(sizes::NAME).unwrap_or_default();\n+    let timing = match timing_str.as_str() {\n+        \"KAPI_SIGNAL_TIME_BEFORE\" | \"before\" =\u003e 0u32,\n+        \"KAPI_SIGNAL_TIME_DURING\" | \"during\" =\u003e 1,\n+        \"KAPI_SIGNAL_TIME_AFTER\" | \"after\" =\u003e 2,\n+        _ =\u003e 0,\n+    };\n+    let priority = reader.read_u8()?;\n+    let interruptible = reader.read_bool()?;\n+    let queue_behavior = reader.read_optional_string(sizes::NAME);\n+    let state_required = reader.read_u32()?;\n+    let state_forbidden = reader.read_u32()?;\n+\n+    Some(SignalSpec {\n+        signal_num,\n+        signal_name,\n+        direction,\n+        action,\n+        target,\n+        condition,\n+        description,\n+        timing,\n+        priority: priority as u32,\n+        restartable,\n+        interruptible,\n+        queue: queue_behavior,\n+        sa_flags: 0, // Not a field of struct kapi_signal_spec\n+        sa_flags_required,\n+        sa_flags_forbidden,\n+        state_required,\n+        state_forbidden,\n+        // `error_on_signal` of 0 means \"no errno returned\"; surface\n+        // that as None to match the source-parser convention.\n+        error_on_signal: if error_on_signal != 0 {\n+            Some(error_on_signal)\n+        } else {\n+            None\n+        },\n+        transform_to: if transform_to != 0 {\n+            // The compiled struct holds the numeric value; the C\n+            // preprocessor already resolved any signal symbol.\n+            Some(transform_to)\n+        } else {\n+            None\n+        },\n+    })\n+}\n+\n+fn parse_signal_mask(reader: \u0026mut DataReader) -\u003e Option\u003cSignalMaskSpec\u003e {\n+    let name = reader.read_string_or_default(sizes::NAME);\n+\n+    let mut signals = Vec::with_capacity(sizes::MAX_SIGNALS);\n+    for _ in 0..sizes::MAX_SIGNALS {\n+        signals.push(reader.read_i32()?);\n+    }\n+    let signal_count = reader.read_u32()?;\n+    signals.truncate(signal_count as usize);\n+\n+    let description = reader.read_string_or_default(sizes::DESC);\n+\n+    Some(SignalMaskSpec {\n+        name,\n+        description,\n+        signals,\n+    })\n+}\n+\n+fn parse_struct_field(reader: \u0026mut DataReader) -\u003e Option\u003cStructFieldSpec\u003e {\n+    let name = reader.read_string_or_default(sizes::NAME);\n+    let field_type = reader.read_u32()?;\n+    let type_name = reader.read_string_or_default(sizes::NAME);\n+    let offset = reader.read_usize()?;\n+    let size = reader.read_usize()?;\n+    let flags = reader.read_u32()?;\n+    let constraint_type = reader.read_u32()?;\n+    let min_value = reader.read_i64()?;\n+    let max_value = reader.read_i64()?;\n+    let valid_mask = reader.read_u64()?;\n+    // enum_values is a `const char *` that StructFieldSpec has no slot for\n+    reader.read_ptr()?;\n+    let description = reader.read_string_or_default(sizes::DESC);\n+\n+    Some(StructFieldSpec {\n+        name,\n+        field_type,\n+        type_name,\n+        offset,\n+        size,\n+        flags,\n+        constraint_type,\n+        min_value,\n+        max_value,\n+        valid_mask,\n+        description,\n+    })\n+}\n+\n+fn parse_struct_spec(reader: \u0026mut DataReader) -\u003e Option\u003cStructSpec\u003e {\n+    let name = reader.read_string_or_default(sizes::NAME);\n+    let size = reader.read_usize()?;\n+    let alignment = reader.read_usize()?;\n+    let field_count = reader.read_u32()?;\n+\n+    let mut fields = Vec::new();\n+    for i in 0..sizes::MAX_PARAMS {\n+        let field = parse_struct_field(reader)?;\n+        reader.align_to(reader.ptr_size());\n+        if i \u003c field_count as usize {\n+            fields.push(field);\n+        }\n+    }\n+\n+    let description = reader.read_string_or_default(sizes::DESC);\n+\n+    Some(StructSpec {\n+        name,\n+        size,\n+        alignment,\n+        field_count: fields.len() as u32,\n+        fields,\n+        description,\n+    })\n+}\n+\n+fn parse_side_effect(reader: \u0026mut DataReader) -\u003e Option\u003cSideEffectSpec\u003e {\n+    let effect_type = reader.read_u32()?;\n+    let target = reader.read_string_or_default(sizes::NAME);\n+    let condition = reader.read_string_or_default(sizes::DESC);\n+    let description = reader.read_string_or_default(sizes::DESC);\n+    let reversible = reader.read_bool()?;\n+\n+    Some(SideEffectSpec {\n+        effect_type,\n+        target,\n+        condition: opt_string(condition),\n+        description,\n+        reversible,\n+    })\n+}\n+\n+fn parse_state_transition(reader: \u0026mut DataReader) -\u003e Option\u003cStateTransitionSpec\u003e {\n+    let from_state = reader.read_string_or_default(sizes::NAME);\n+    let to_state = reader.read_string_or_default(sizes::NAME);\n+    let condition = reader.read_string_or_default(sizes::DESC);\n+    let object = reader.read_string_or_default(sizes::NAME);\n+    let description = reader.read_string_or_default(sizes::DESC);\n+\n+    Some(StateTransitionSpec {\n+        object,\n+        from_state,\n+        to_state,\n+        condition: opt_string(condition),\n+        description,\n+    })\n+}\n+\n+fn parse_capability(reader: \u0026mut DataReader) -\u003e Option\u003cCapabilitySpec\u003e {\n+    // Struct layout matches `struct kapi_capability_spec`:\n+    //   int capability; const char *cap_name; enum action;\n+    //   const char *allows; const char *without_cap;\n+    //   const char *check_condition; u8 priority;\n+    //   int alternative[KAPI_MAX_CAPABILITIES]; u32 alternative_count;\n+    let capability = reader.read_i32()?;\n+    let cap_name = reader.read_string_or_default(sizes::NAME);\n+    let action = reader.read_u32()?;\n+    let allows = reader.read_string_or_default(sizes::DESC);\n+    let without_cap = reader.read_string_or_default(sizes::DESC);\n+    let check_condition = reader.read_optional_string(sizes::DESC);\n+    let priority = reader.read_u8()?;\n+\n+    let mut alternatives = Vec::with_capacity(sizes::MAX_CAPABILITIES);\n+    for _ in 0..sizes::MAX_CAPABILITIES {\n+        alternatives.push(reader.read_i32()?);\n+    }\n+    let alternative_count = reader.read_u32()?;\n+    alternatives.truncate(alternative_count as usize);\n+\n+    Some(CapabilitySpec {\n+        capability,\n+        name: cap_name,\n+        action: capability_action_to_string(action),\n+        allows,\n+        without_cap,\n+        check_condition,\n+        priority: Some(priority),\n+        alternatives,\n+    })\n+}\n+\n+/// Map the `enum kapi_capability_action` numeric value to its symbolic\n+/// spelling, matching `include/linux/kernel_api_spec.h`.\n+fn capability_action_to_string(n: u32) -\u003e String {\n+    match n {\n+        0 =\u003e \"KAPI_CAP_BYPASS_CHECK\",\n+        1 =\u003e \"KAPI_CAP_INCREASE_LIMIT\",\n+        2 =\u003e \"KAPI_CAP_OVERRIDE_RESTRICTION\",\n+        3 =\u003e \"KAPI_CAP_GRANT_PERMISSION\",\n+        4 =\u003e \"KAPI_CAP_MODIFY_BEHAVIOR\",\n+        5 =\u003e \"KAPI_CAP_ACCESS_RESOURCE\",\n+        6 =\u003e \"KAPI_CAP_PERFORM_OPERATION\",\n+        _ =\u003e return n.to_string(),\n+    }\n+    .to_string()\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+\n+    #[test]\n+    fn state_transition_with_empty_strings_consumes_the_whole_struct() {\n+        // five `const char *` slots: from_state, to_state, condition,\n+        // object, description\n+        let data = [0u8; 5 * 8];\n+        let mut reader = DataReader::new(\u0026data, 0, Endian::Little, true);\n+\n+        let trans = parse_state_transition(\u0026mut reader).unwrap();\n+\n+        assert_eq!(reader.pos, data.len());\n+        assert_eq!(trans.from_state, \"\");\n+        assert_eq!(trans.condition, None);\n+        assert_eq!(trans.description, \"\");\n+    }\n+\n+    // Offsets inside `struct kernel_api_spec` and its element structs as\n+    // laid out by gcc on x86-64, taken from offsetof()/sizeof().\n+    const SPEC_SIZE: usize = 26400;\n+    const PARAM_MAGIC: usize = 36;\n+    const PARAMS: usize = 48;\n+    const PARAM_SIZE: usize = 120;\n+    const RETURN_MAGIC: usize = 1968;\n+    const RETURN_SPEC: usize = 1976;\n+    const ERROR_MAGIC: usize = 2048;\n+    const ERRORS: usize = 2056;\n+    const ERROR_SIZE: usize = 32;\n+    const LOCK_MAGIC: usize = 3080;\n+    const LOCKS: usize = 3088;\n+    const LOCK_SIZE: usize = 24;\n+    const CONSTRAINT_MAGIC: usize = 3472;\n+    const CONSTRAINTS: usize = 3480;\n+    const CONSTRAINT_SIZE: usize = 24;\n+    const INFO_MAGIC: usize = 4248;\n+    const NOTES: usize = 4264;\n+    const SIGNAL_MAGIC: usize = 4272;\n+    const SIGNALS: usize = 4280;\n+    const SIGNAL_SIZE: usize = 104;\n+    const SIGMASK_MAGIC: usize = 7608;\n+    const SIGNAL_MASKS: usize = 7616;\n+    const SIGNAL_MASK_SIZE: usize = 152;\n+    const STRUCT_MAGIC: usize = 12480;\n+    const STRUCT_SPECS: usize = 12488;\n+    const STRUCT_SPEC_SIZE: usize = 1448;\n+    const STRUCT_FIELD_SIZE: usize = 88;\n+    const EFFECT_MAGIC: usize = 24072;\n+    const SIDE_EFFECTS: usize = 24080;\n+    const SIDE_EFFECT_SIZE: usize = 40;\n+    const TRANS_MAGIC: usize = 25360;\n+    const STATE_TRANSITIONS: usize = 25368;\n+    const STATE_TRANSITION_SIZE: usize = 40;\n+    const CAP_MAGIC: usize = 25688;\n+    const CAPABILITIES: usize = 25696;\n+    const CAPABILITY_SIZE: usize = 88;\n+\n+    const BASE: u64 = 0xffff_ffff_8100_0000;\n+    const CONTENT_OFFSET: usize = 0x1000;\n+\n+    /// A `struct kernel_api_spec` image preceded by the strings and\n+    /// `s64` arrays it points at, wrapped in a minimal ELF file.\n+    struct Image {\n+        spec: Vec\u003cu8\u003e,\n+        tail: Vec\u003cu8\u003e,\n+    }\n+\n+    impl Image {\n+        fn new() -\u003e Self {\n+            Image {\n+                spec: vec![0; SPEC_SIZE],\n+                tail: Vec::new(),\n+            }\n+        }\n+\n+        fn u32(\u0026mut self, at: usize, v: u32) {\n+            self.spec[at..at + 4].copy_from_slice(\u0026v.to_le_bytes());\n+        }\n+\n+        fn u64(\u0026mut self, at: usize, v: u64) {\n+            self.spec[at..at + 8].copy_from_slice(\u0026v.to_le_bytes());\n+        }\n+\n+        fn tail_vaddr(\u0026self) -\u003e u64 {\n+            BASE + self.tail.len() as u64\n+        }\n+\n+        fn string(\u0026mut self, at: usize, s: \u0026str) {\n+            let vaddr = self.tail_vaddr();\n+            self.tail.extend_from_slice(s.as_bytes());\n+            self.tail.push(0);\n+            self.u64(at, vaddr);\n+        }\n+\n+        fn s64s(\u0026mut self, at: usize, vals: \u0026[i64]) {\n+            while self.tail.len() % 8 != 0 {\n+                self.tail.push(0);\n+            }\n+            let vaddr = self.tail_vaddr();\n+            for v in vals {\n+                self.tail.extend_from_slice(\u0026v.to_le_bytes());\n+            }\n+            self.u64(at, vaddr);\n+        }\n+\n+        fn elf(\u0026self) -\u003e Vec\u003cu8\u003e {\n+            let mut content = self.tail.clone();\n+            content.extend_from_slice(\u0026self.spec);\n+\n+            let mut img = vec![0u8; CONTENT_OFFSET];\n+            img[..4].copy_from_slice(b\"\\x7fELF\");\n+            img[4] = 2; // ELFCLASS64\n+            img[5] = 1; // ELFDATA2LSB\n+            img[6] = 1; // EV_CURRENT\n+            img[16..18].copy_from_slice(\u00262u16.to_le_bytes()); // ET_EXEC\n+            img[18..20].copy_from_slice(\u002662u16.to_le_bytes()); // EM_X86_64\n+            img[20..24].copy_from_slice(\u00261u32.to_le_bytes());\n+            img[40..48].copy_from_slice(\u002664u64.to_le_bytes()); // e_shoff\n+            img[52..54].copy_from_slice(\u002664u16.to_le_bytes()); // e_ehsize\n+            img[58..60].copy_from_slice(\u002664u16.to_le_bytes()); // e_shentsize\n+            img[60..62].copy_from_slice(\u00262u16.to_le_bytes()); // e_shnum\n+            let sh = 64 + 64; // section 1 follows the null section header at 64\n+            img[sh + 4..sh + 8].copy_from_slice(\u00261u32.to_le_bytes()); // SHT_PROGBITS\n+            img[sh + 8..sh + 16].copy_from_slice(\u00262u64.to_le_bytes()); // SHF_ALLOC\n+            img[sh + 16..sh + 24].copy_from_slice(\u0026BASE.to_le_bytes());\n+            img[sh + 24..sh + 32].copy_from_slice(\u0026(CONTENT_OFFSET as u64).to_le_bytes());\n+            img[sh + 32..sh + 40].copy_from_slice(\u0026(content.len() as u64).to_le_bytes());\n+            img[sh + 48..sh + 56].copy_from_slice(\u00268u64.to_le_bytes());\n+            img.extend_from_slice(\u0026content);\n+            img\n+        }\n+\n+        fn parse(\u0026mut self) -\u003e Result\u003cApiSpec\u003e {\n+            self.string(0, \"kapi_fixture\");\n+            while self.tail.len() % 8 != 0 {\n+                self.tail.push(0);\n+            }\n+            let img = self.elf();\n+            parse_binary_to_api_spec(\u0026img, CONTENT_OFFSET + self.tail.len(), Endian::Little, true)\n+        }\n+    }\n+\n+    #[test]\n+    fn every_array_slot_is_consumed() {\n+        let mut img = Image::new();\n+        img.u32(PARAM_MAGIC, magic::PARAMS);\n+        img.u32(PARAM_MAGIC + 4, 16);\n+        img.u32(RETURN_MAGIC, magic::RETURN);\n+        img.u32(ERROR_MAGIC, magic::ERRORS);\n+        img.u32(ERROR_MAGIC + 4, 32);\n+        img.u32(LOCK_MAGIC, magic::LOCKS);\n+        img.u32(LOCK_MAGIC + 4, 16);\n+        img.u32(CONSTRAINT_MAGIC, magic::CONSTRAINTS);\n+        img.u32(CONSTRAINT_MAGIC + 4, 32);\n+        img.u32(SIGNAL_MAGIC, magic::SIGNALS);\n+        img.u32(SIGNAL_MAGIC + 4, 32);\n+        img.u32(SIGMASK_MAGIC, magic::SIGMASK);\n+        img.u32(SIGMASK_MAGIC + 4, 32);\n+        img.u32(STRUCT_MAGIC, magic::STRUCTS);\n+        img.u32(STRUCT_MAGIC + 4, 8);\n+        img.u32(EFFECT_MAGIC, magic::EFFECTS);\n+        img.u32(EFFECT_MAGIC + 4, 32);\n+        img.u32(TRANS_MAGIC, magic::TRANS);\n+        img.u32(TRANS_MAGIC + 4, 8);\n+        img.u32(CAP_MAGIC, magic::CAPS);\n+        img.u32(CAP_MAGIC + 4, 8);\n+\n+        img.string(PARAMS + 15 * PARAM_SIZE, \"last_param\");\n+        img.u32(ERRORS + 31 * ERROR_SIZE, -7i32 as u32);\n+        img.string(ERRORS + 31 * ERROR_SIZE + 8, \"ELAST\");\n+        img.string(LOCKS + 15 * LOCK_SIZE, \"last_lock\");\n+        img.string(CONSTRAINTS + 31 * CONSTRAINT_SIZE, \"last_constraint\");\n+        img.u32(SIGNALS + 31 * SIGNAL_SIZE, 31);\n+        img.u32(SIGNAL_MASKS + 31 * SIGNAL_MASK_SIZE + 8, 64);\n+        img.u32(SIGNAL_MASKS + 31 * SIGNAL_MASK_SIZE + 136, 1);\n+        img.string(SIGNAL_MASKS + 31 * SIGNAL_MASK_SIZE, \"last mask\");\n+        img.string(SIGNAL_MASKS + 31 * SIGNAL_MASK_SIZE + 144, \"mask desc\");\n+\n+        let last_struct = STRUCT_SPECS + 7 * STRUCT_SPEC_SIZE;\n+        img.string(last_struct, \"last_struct\");\n+        img.u32(last_struct + 24, 16);\n+        img.string(last_struct + 32 + 15 * STRUCT_FIELD_SIZE, \"last_field\");\n+        img.string(last_struct + 32 + 15 * STRUCT_FIELD_SIZE + 80, \"field desc\");\n+        img.string(last_struct + 1440, \"struct desc\");\n+\n+        img.string(SIDE_EFFECTS + 31 * SIDE_EFFECT_SIZE + 8, \"last_effect\");\n+        img.string(\n+            STATE_TRANSITIONS + 7 * STATE_TRANSITION_SIZE + 24,\n+            \"last_object\",\n+        );\n+        let last_cap = CAPABILITIES + 7 * CAPABILITY_SIZE;\n+        img.u32(last_cap, 40);\n+        img.u32(last_cap + 52 + 7 * 4, 99);\n+        img.u32(last_cap + 84, 8);\n+\n+        let spec = img.parse().unwrap();\n+\n+        assert_eq!(spec.parameters.len(), 16);\n+        assert_eq!(spec.parameters[15].name, \"last_param\");\n+        assert_eq!(spec.errors.len(), 32);\n+        assert_eq!(spec.errors[31].error_code, -7);\n+        assert_eq!(spec.errors[31].name, \"ELAST\");\n+        assert_eq!(spec.locks[15].lock_name, \"last_lock\");\n+        assert_eq!(spec.constraints[31].name, \"last_constraint\");\n+        assert_eq!(spec.signals[31].signal_num, 31);\n+        assert_eq!(spec.signal_masks.len(), 32);\n+        assert_eq!(spec.signal_masks[31].name, \"last mask\");\n+        assert_eq!(spec.signal_masks[31].signals, [64]);\n+        assert_eq!(spec.signal_masks[31].description, \"mask desc\");\n+\n+        let st = \u0026spec.struct_specs[7];\n+        assert_eq!(st.name, \"last_struct\");\n+        assert_eq!(st.description, \"struct desc\");\n+        assert_eq!(st.field_count, 16);\n+        assert_eq!(st.fields[15].name, \"last_field\");\n+        assert_eq!(st.fields[15].description, \"field desc\");\n+\n+        assert_eq!(spec.side_effects[31].target, \"last_effect\");\n+        assert_eq!(spec.state_transitions[7].object, \"last_object\");\n+        let cap = \u0026spec.capabilities[7];\n+        assert_eq!(cap.capability, 40);\n+        assert_eq!(cap.alternatives, [0, 0, 0, 0, 0, 0, 0, 99]);\n+    }\n+\n+    #[test]\n+    fn notes_and_signal_masks_are_read_without_their_markers() {\n+        let mut img = Image::new();\n+        img.u32(SIGMASK_MAGIC + 4, 2);\n+        img.string(NOTES, \"only notes\");\n+        img.string(SIGNAL_MASKS, \"first\");\n+        img.u32(SIGNAL_MASKS + 8, 2);\n+        img.u32(SIGNAL_MASKS + 12, 15);\n+        img.u32(SIGNAL_MASKS + 136, 2);\n+        img.string(SIGNAL_MASKS + 144, \"first desc\");\n+        img.string(SIGNAL_MASKS + SIGNAL_MASK_SIZE, \"second\");\n+\n+        let spec = img.parse().unwrap();\n+\n+        assert_eq!(spec.examples, None);\n+        assert_eq!(spec.notes.as_deref(), Some(\"only notes\"));\n+        assert_eq!(spec.signal_masks.len(), 2);\n+        assert_eq!(spec.signal_masks[0].name, \"first\");\n+        assert_eq!(spec.signal_masks[0].description, \"first desc\");\n+        assert_eq!(spec.signal_masks[0].signals, [2, 15]);\n+        assert_eq!(spec.signal_masks[1].name, \"second\");\n+        assert!(spec.signal_masks[1].signals.is_empty());\n+    }\n+\n+    #[test]\n+    fn examples_are_read_next_to_notes() {\n+        let mut img = Image::new();\n+        img.u32(INFO_MAGIC, magic::INFO);\n+        img.string(INFO_MAGIC + 8, \"an example\");\n+        img.string(NOTES, \"a note\");\n+\n+        let spec = img.parse().unwrap();\n+\n+        assert_eq!(spec.examples.as_deref(), Some(\"an example\"));\n+        assert_eq!(spec.notes.as_deref(), Some(\"a note\"));\n+    }\n+\n+    #[test]\n+    fn enum_and_error_values_are_followed_through_their_pointers() {\n+        let mut img = Image::new();\n+        img.u32(PARAM_MAGIC, magic::PARAMS);\n+        img.u32(PARAM_MAGIC + 4, 2);\n+        img.string(PARAMS, \"mode\");\n+        img.s64s(PARAMS + 64, \u0026[0, 1, -7, 1 \u003c\u003c 40]);\n+        img.u32(PARAMS + 72, 4);\n+        img.string(PARAMS + PARAM_SIZE, \"plain\");\n+        img.u32(RETURN_MAGIC, magic::RETURN);\n+        img.string(RETURN_SPEC, \"long\");\n+        img.u32(RETURN_SPEC + 12, 2);\n+        img.s64s(RETURN_SPEC + 40, \u0026[-22, -2, -(1 \u003c\u003c 40)]);\n+        img.u32(RETURN_SPEC + 48, 3);\n+\n+        let spec = img.parse().unwrap();\n+\n+        assert_eq!(\n+            spec.parameters[0].enum_values,\n+            [\"0\", \"1\", \"-7\", \"1099511627776\"]\n+        );\n+        assert!(spec.parameters[1].enum_values.is_empty());\n+        let ret = spec.return_spec.unwrap();\n+        assert_eq!(ret.check_type, 2);\n+        assert_eq!(ret.error_values, [-22, -2]);\n+    }\n+\n+    #[test]\n+    fn numbers_the_constraint_does_not_use_are_unset() {\n+        let mut img = Image::new();\n+        img.u32(PARAM_MAGIC, magic::PARAMS);\n+        img.u32(PARAM_MAGIC + 4, 4);\n+        img.string(PARAMS, \"plain\");\n+        let ranged = PARAMS + PARAM_SIZE;\n+        img.string(ranged, \"ranged\");\n+        img.u64(ranged + 24, 16);\n+        img.u64(ranged + 40, -5i64 as u64);\n+        img.u64(ranged + 48, 9);\n+        img.u64(ranged + 56, 0xff);\n+        img.u32(ranged + 76, 1);\n+        let string = PARAMS + 2 * PARAM_SIZE;\n+        img.string(string, \"string\");\n+        img.u64(string + 40, 1);\n+        img.u64(string + 48, 255);\n+        img.u32(string + 76, 8);\n+        let unlimited = PARAMS + 3 * PARAM_SIZE;\n+        img.string(unlimited, \"unlimited\");\n+        img.u32(unlimited + 76, 8);\n+        img.u32(RETURN_MAGIC, magic::RETURN);\n+        img.string(RETURN_SPEC, \"long\");\n+        img.u32(RETURN_SPEC + 12, 1);\n+        img.u64(RETURN_SPEC + 16, 7);\n+        img.u64(RETURN_SPEC + 32, 100);\n+\n+        let spec = img.parse().unwrap();\n+\n+        let plain = \u0026spec.parameters[0];\n+        assert_eq!((plain.min_value, plain.max_value), (None, None));\n+        assert_eq!(\n+            (plain.valid_mask, plain.size, plain.alignment),\n+            (None, None, None)\n+        );\n+        let ranged = \u0026spec.parameters[1];\n+        assert_eq!((ranged.min_value, ranged.max_value), (Some(-5), Some(9)));\n+        assert_eq!((ranged.valid_mask, ranged.size), (None, Some(16)));\n+        let string = \u0026spec.parameters[2];\n+        assert_eq!((string.min_value, string.max_value), (Some(1), Some(255)));\n+        let unlimited = \u0026spec.parameters[3];\n+        assert_eq!((unlimited.min_value, unlimited.max_value), (None, None));\n+        let ret = spec.return_spec.unwrap();\n+        assert_eq!(ret.success_value, None);\n+        assert_eq!((ret.success_min, ret.success_max), (Some(0), Some(100)));\n+    }\n+\n+    #[test]\n+    fn enum_values_behind_a_dangling_pointer_are_dropped() {\n+        let mut img = Image::new();\n+        img.u32(PARAM_MAGIC, magic::PARAMS);\n+        img.u32(PARAM_MAGIC + 4, 1);\n+        img.string(PARAMS, \"mode\");\n+        img.u64(PARAMS + 64, 0x1000);\n+        img.u32(PARAMS + 72, 4);\n+\n+        let spec = img.parse().unwrap();\n+\n+        assert!(spec.parameters[0].enum_values.is_empty());\n+    }\n+\n+    #[test]\n+    fn unexpected_section_marker_is_rejected() {\n+        let mut img = Image::new();\n+        img.u32(ERROR_MAGIC, 0xdead_beef);\n+\n+        let err = img.parse().unwrap_err();\n+\n+        assert!(err.to_string().contains(\"0xdeadbeef\"), \"{err}\");\n+    }\n+\n+    #[test]\n+    fn truncated_spec_is_rejected() {\n+        let mut img = Image::new();\n+        img.spec.truncate(SIGNAL_MASKS);\n+\n+        let err = img.parse().unwrap_err();\n+\n+        assert!(err.to_string().contains(\"past the end\"), \"{err}\");\n+    }\n+}\ndiff --git a/tools/kapi/src/formatter/json.rs b/tools/kapi/src/formatter/json.rs\nnew file mode 100644\nindex 0000000000000..5f2367ad12d40\n--- /dev/null\n+++ b/tools/kapi/src/formatter/json.rs\n@@ -0,0 +1,659 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use super::OutputFormatter;\n+use crate::extractor::{\n+    CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec, SideEffectSpec,\n+    SignalMaskSpec, SignalSpec, StateTransitionSpec, StructSpec,\n+};\n+use serde::Serialize;\n+use std::io::Write;\n+\n+pub struct JsonFormatter {\n+    data: JsonData,\n+}\n+\n+#[derive(Serialize)]\n+struct JsonData {\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    apis: Option\u003cVec\u003cJsonApi\u003e\u003e,\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    api_details: Option\u003cJsonApiDetails\u003e,\n+}\n+\n+#[derive(Serialize)]\n+struct JsonApi {\n+    name: String,\n+    api_type: String,\n+}\n+\n+#[derive(Serialize)]\n+struct JsonApiDetails {\n+    name: String,\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    description: Option\u003cString\u003e,\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    long_description: Option\u003cString\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    context_flags: Vec\u003cString\u003e,\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    examples: Option\u003cString\u003e,\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    notes: Option\u003cString\u003e,\n+    // Sysfs-specific fields\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    subsystem: Option\u003cString\u003e,\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    sysfs_path: Option\u003cString\u003e,\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    permissions: Option\u003cString\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    capabilities: Vec\u003cCapabilitySpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    state_transitions: Vec\u003cStateTransitionSpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    side_effects: Vec\u003cSideEffectSpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    parameters: Vec\u003cParamSpec\u003e,\n+    #[serde(skip_serializing_if = \"Option::is_none\")]\n+    return_spec: Option\u003cReturnSpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    errors: Vec\u003cErrorSpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    locks: Vec\u003cLockSpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    struct_specs: Vec\u003cStructSpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    signals: Vec\u003cSignalSpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    signal_masks: Vec\u003cSignalMaskSpec\u003e,\n+    #[serde(skip_serializing_if = \"Vec::is_empty\")]\n+    constraints: Vec\u003cConstraintSpec\u003e,\n+}\n+\n+impl JsonFormatter {\n+    pub fn new() -\u003e Self {\n+        JsonFormatter {\n+            data: JsonData {\n+                apis: None,\n+                api_details: None,\n+            },\n+        }\n+    }\n+}\n+\n+impl OutputFormatter for JsonFormatter {\n+    fn begin_document(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn end_document(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        let json = serde_json::to_string_pretty(\u0026self.data)?;\n+        writeln!(w, \"{json}\")?;\n+        Ok(())\n+    }\n+\n+    fn begin_api_list(\u0026mut self, _w: \u0026mut dyn Write, _title: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if self.data.apis.is_none() {\n+            self.data.apis = Some(Vec::new());\n+        }\n+        Ok(())\n+    }\n+\n+    fn api_item(\u0026mut self, _w: \u0026mut dyn Write, name: \u0026str, api_type: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(apis) = \u0026mut self.data.apis {\n+            apis.push(JsonApi {\n+                name: name.to_string(),\n+                api_type: api_type.to_string(),\n+            });\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_api_list(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn total_specs(\u0026mut self, _w: \u0026mut dyn Write, _count: usize) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_api_details(\u0026mut self, _w: \u0026mut dyn Write, name: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        self.data.api_details = Some(JsonApiDetails {\n+            name: name.to_string(),\n+            description: None,\n+            long_description: None,\n+            context_flags: Vec::new(),\n+            examples: None,\n+            notes: None,\n+            subsystem: None,\n+            sysfs_path: None,\n+            permissions: None,\n+            capabilities: Vec::new(),\n+            state_transitions: Vec::new(),\n+            side_effects: Vec::new(),\n+            parameters: Vec::new(),\n+            return_spec: None,\n+            errors: Vec::new(),\n+            locks: Vec::new(),\n+            struct_specs: Vec::new(),\n+            signals: Vec::new(),\n+            signal_masks: Vec::new(),\n+            constraints: Vec::new(),\n+        });\n+        Ok(())\n+    }\n+\n+    fn end_api_details(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn description(\u0026mut self, _w: \u0026mut dyn Write, desc: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.description = Some(desc.to_string());\n+        }\n+        Ok(())\n+    }\n+\n+    fn long_description(\u0026mut self, _w: \u0026mut dyn Write, desc: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.long_description = Some(desc.to_string());\n+        }\n+        Ok(())\n+    }\n+\n+    fn begin_context_flags(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn context_flag(\u0026mut self, _w: \u0026mut dyn Write, flag: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.context_flags.push(flag.to_string());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_context_flags(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_parameters(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn end_parameters(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_errors(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn end_errors(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn examples(\u0026mut self, _w: \u0026mut dyn Write, examples: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.examples = Some(examples.to_string());\n+        }\n+        Ok(())\n+    }\n+\n+    fn notes(\u0026mut self, _w: \u0026mut dyn Write, notes: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.notes = Some(notes.to_string());\n+        }\n+        Ok(())\n+    }\n+\n+    fn sysfs_subsystem(\u0026mut self, _w: \u0026mut dyn Write, subsystem: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.subsystem = Some(subsystem.to_string());\n+        }\n+        Ok(())\n+    }\n+\n+    fn sysfs_path(\u0026mut self, _w: \u0026mut dyn Write, path: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.sysfs_path = Some(path.to_string());\n+        }\n+        Ok(())\n+    }\n+\n+    fn sysfs_permissions(\u0026mut self, _w: \u0026mut dyn Write, perms: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.permissions = Some(perms.to_string());\n+        }\n+        Ok(())\n+    }\n+\n+    fn begin_capabilities(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn capability(\u0026mut self, _w: \u0026mut dyn Write, cap: \u0026CapabilitySpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.capabilities.push(cap.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_capabilities(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn parameter(\u0026mut self, _w: \u0026mut dyn Write, param: \u0026ParamSpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.parameters.push(param.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn return_spec(\u0026mut self, _w: \u0026mut dyn Write, ret: \u0026ReturnSpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.return_spec = Some(ret.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn error(\u0026mut self, _w: \u0026mut dyn Write, error: \u0026ErrorSpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.errors.push(error.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn begin_signals(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn signal(\u0026mut self, _w: \u0026mut dyn Write, signal: \u0026SignalSpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(api_details) = \u0026mut self.data.api_details {\n+            api_details.signals.push(signal.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_signals(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_signal_masks(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn signal_mask(\u0026mut self, _w: \u0026mut dyn Write, mask: \u0026SignalMaskSpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(api_details) = \u0026mut self.data.api_details {\n+            api_details.signal_masks.push(mask.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_signal_masks(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_side_effects(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn side_effect(\u0026mut self, _w: \u0026mut dyn Write, effect: \u0026SideEffectSpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.side_effects.push(effect.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_side_effects(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_state_transitions(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn state_transition(\n+        \u0026mut self,\n+        _w: \u0026mut dyn Write,\n+        trans: \u0026StateTransitionSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.state_transitions.push(trans.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_state_transitions(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_constraints(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn constraint(\n+        \u0026mut self,\n+        _w: \u0026mut dyn Write,\n+        constraint: \u0026ConstraintSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(api_details) = \u0026mut self.data.api_details {\n+            api_details.constraints.push(constraint.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_constraints(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_locks(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn lock(\u0026mut self, _w: \u0026mut dyn Write, lock: \u0026LockSpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(details) = \u0026mut self.data.api_details {\n+            details.locks.push(lock.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_locks(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_struct_specs(\u0026mut self, _w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn struct_spec(\u0026mut self, _w: \u0026mut dyn Write, spec: \u0026StructSpec) -\u003e std::io::Result\u003c()\u003e {\n+        if let Some(ref mut details) = self.data.api_details {\n+            details.struct_specs.push(spec.clone());\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_struct_specs(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+    use crate::extractor::{ErrorSpec, ParamSpec, ReturnSpec};\n+\n+    fn render_json(f: \u0026mut JsonFormatter) -\u003e String {\n+        let mut buf = Vec::new();\n+        f.end_document(\u0026mut buf).unwrap();\n+        String::from_utf8(buf).unwrap()\n+    }\n+\n+    #[test]\n+    fn json_output_is_valid() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.description(\u0026mut sink, \"A test syscall\").unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+\n+        // Verify it parses as valid JSON\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+        assert_eq!(parsed[\"api_details\"][\"name\"].as_str(), Some(\"sys_test\"));\n+        assert_eq!(\n+            parsed[\"api_details\"][\"description\"].as_str(),\n+            Some(\"A test syscall\")\n+        );\n+    }\n+\n+    #[test]\n+    fn json_api_list() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_list(\u0026mut sink, \"Syscalls\").unwrap();\n+        f.api_item(\u0026mut sink, \"sys_open\", \"syscall\").unwrap();\n+        f.api_item(\u0026mut sink, \"sys_read\", \"syscall\").unwrap();\n+        f.end_api_list(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+\n+        let apis = parsed[\"apis\"].as_array().unwrap();\n+        assert_eq!(apis.len(), 2);\n+        assert_eq!(apis[0][\"name\"].as_str(), Some(\"sys_open\"));\n+        assert_eq!(apis[0][\"api_type\"].as_str(), Some(\"syscall\"));\n+        assert_eq!(apis[1][\"name\"].as_str(), Some(\"sys_read\"));\n+    }\n+\n+    #[test]\n+    fn json_special_characters_in_description() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.description(\u0026mut sink, \"Contains \\\"quotes\\\" and \\\\backslashes\\\\\")\n+            .unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+\n+        // Must be valid JSON despite special characters\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+        assert_eq!(\n+            parsed[\"api_details\"][\"description\"].as_str(),\n+            Some(\"Contains \\\"quotes\\\" and \\\\backslashes\\\\\")\n+        );\n+    }\n+\n+    #[test]\n+    fn json_special_characters_in_name() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_list(\u0026mut sink, \"APIs\").unwrap();\n+        // Names with underscores (common in kernel) and unusual strings\n+        f.api_item(\u0026mut sink, \"sys_new\\tline\", \"syscall\").unwrap();\n+        f.end_api_list(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+\n+        // Must parse correctly; serde_json handles escaping for us\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+        assert_eq!(parsed[\"apis\"][0][\"name\"].as_str(), Some(\"sys_new\\tline\"));\n+    }\n+\n+    #[test]\n+    fn json_parameters_serialized() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_write\").unwrap();\n+        f.begin_parameters(\u0026mut sink, 2).unwrap();\n+        f.parameter(\n+            \u0026mut sink,\n+            \u0026ParamSpec {\n+                index: 0,\n+                name: \"fd\".to_string(),\n+                type_name: \"unsigned int\".to_string(),\n+                description: \"file descriptor\".to_string(),\n+                flags: 1,\n+                param_type: 2,\n+                constraint_type: 0,\n+                constraint: None,\n+                min_value: Some(0),\n+                max_value: Some(1024),\n+                valid_mask: None,\n+                enum_values: vec![],\n+                size: None,\n+                alignment: None,\n+                size_param_idx: None,\n+            },\n+        )\n+        .unwrap();\n+        f.end_parameters(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+\n+        let params = parsed[\"api_details\"][\"parameters\"].as_array().unwrap();\n+        assert_eq!(params.len(), 1);\n+        assert_eq!(params[0][\"name\"].as_str(), Some(\"fd\"));\n+        assert_eq!(params[0][\"param_type\"].as_u64(), Some(2));\n+    }\n+\n+    #[test]\n+    fn json_errors_serialized() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_read\").unwrap();\n+        f.begin_errors(\u0026mut sink, 1).unwrap();\n+        f.error(\n+            \u0026mut sink,\n+            \u0026ErrorSpec {\n+                error_code: -9,\n+                name: \"EBADF\".to_string(),\n+                condition: \"fd is not valid\".to_string(),\n+                description: \"Bad file descriptor\".to_string(),\n+            },\n+        )\n+        .unwrap();\n+        f.end_errors(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+\n+        let errors = parsed[\"api_details\"][\"errors\"].as_array().unwrap();\n+        assert_eq!(errors.len(), 1);\n+        assert_eq!(errors[0][\"name\"].as_str(), Some(\"EBADF\"));\n+        assert_eq!(errors[0][\"error_code\"].as_i64(), Some(-9));\n+    }\n+\n+    #[test]\n+    fn json_empty_details_omits_empty_fields() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_empty\").unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+\n+        // description should not be present (skip_serializing_if = Option::is_none)\n+        assert!(parsed[\"api_details\"][\"description\"].is_null());\n+        // parameters empty array should not be present (skip_serializing_if = Vec::is_empty)\n+        assert!(parsed[\"api_details\"][\"parameters\"].is_null());\n+        // errors empty array should not be present\n+        assert!(parsed[\"api_details\"][\"errors\"].is_null());\n+    }\n+\n+    #[test]\n+    fn json_braces_balance() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_balanced\").unwrap();\n+        f.description(\u0026mut sink, \"Test braces balance\").unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+\n+        let open_braces = json.chars().filter(|\u0026c| c == '{').count();\n+        let close_braces = json.chars().filter(|\u0026c| c == '}').count();\n+        assert_eq!(open_braces, close_braces, \"Braces are unbalanced\");\n+\n+        let open_brackets = json.chars().filter(|\u0026c| c == '[').count();\n+        let close_brackets = json.chars().filter(|\u0026c| c == ']').count();\n+        assert_eq!(open_brackets, close_brackets, \"Brackets are unbalanced\");\n+    }\n+\n+    #[test]\n+    fn json_return_spec_serialized() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_open\").unwrap();\n+        f.return_spec(\n+            \u0026mut sink,\n+            \u0026ReturnSpec {\n+                type_name: \"int\".to_string(),\n+                description: \"file descriptor on success\".to_string(),\n+                return_type: 1,\n+                check_type: 3,\n+                success_value: Some(0),\n+                success_min: None,\n+                success_max: None,\n+                error_values: vec![-1],\n+            },\n+        )\n+        .unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+\n+        let ret = \u0026parsed[\"api_details\"][\"return_spec\"];\n+        assert_eq!(ret[\"type_name\"].as_str(), Some(\"int\"));\n+        assert_eq!(ret[\"check_type\"].as_u64(), Some(3));\n+    }\n+\n+    #[test]\n+    fn json_unicode_in_description() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_uni\").unwrap();\n+        f.description(\u0026mut sink, \"Supports unicode: \\u{00e9}\\u{00e8}\\u{00ea}\")\n+            .unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+        assert!(parsed[\"api_details\"][\"description\"]\n+            .as_str()\n+            .unwrap()\n+            .contains('\\u{00e9}'));\n+    }\n+\n+    #[test]\n+    fn json_escapes_embedded_newlines() {\n+        let mut f = JsonFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.long_description(\u0026mut sink, \"One.\\n\\n- a\\n- b\").unwrap();\n+        f.examples(\u0026mut sink, \"a();\\nb();\").unwrap();\n+        f.notes(\u0026mut sink, \"Note one.\\n\\nNote two.\").unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let json = render_json(\u0026mut f);\n+        assert!(json.contains(r#\"\"examples\": \"a();\\nb();\"\"#));\n+        let parsed: serde_json::Value = serde_json::from_str(\u0026json).unwrap();\n+        assert_eq!(\n+            parsed[\"api_details\"][\"long_description\"].as_str(),\n+            Some(\"One.\\n\\n- a\\n- b\")\n+        );\n+        assert_eq!(\n+            parsed[\"api_details\"][\"notes\"].as_str(),\n+            Some(\"Note one.\\n\\nNote two.\")\n+        );\n+    }\n+}\ndiff --git a/tools/kapi/src/formatter/mod.rs b/tools/kapi/src/formatter/mod.rs\nnew file mode 100644\nindex 0000000000000..8d375cb310b0b\n--- /dev/null\n+++ b/tools/kapi/src/formatter/mod.rs\n@@ -0,0 +1,220 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use crate::extractor::{\n+    CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec, SideEffectSpec,\n+    SignalMaskSpec, SignalSpec, StateTransitionSpec, StructSpec,\n+};\n+use std::io::Write;\n+\n+mod json;\n+mod plain;\n+mod rst;\n+\n+pub use json::JsonFormatter;\n+pub use plain::PlainFormatter;\n+pub use rst::RstFormatter;\n+\n+#[derive(Debug, Clone, Copy, PartialEq)]\n+pub enum OutputFormat {\n+    Plain,\n+    Json,\n+    Rst,\n+}\n+\n+impl std::str::FromStr for OutputFormat {\n+    type Err = String;\n+\n+    fn from_str(s: \u0026str) -\u003e Result\u003cSelf, Self::Err\u003e {\n+        match s.to_lowercase().as_str() {\n+            \"plain\" =\u003e Ok(OutputFormat::Plain),\n+            \"json\" =\u003e Ok(OutputFormat::Json),\n+            \"rst\" =\u003e Ok(OutputFormat::Rst),\n+            _ =\u003e Err(format!(\"Unknown output format: {}\", s)),\n+        }\n+    }\n+}\n+\n+/// Names of the KAPI_PARAM_* flags set in `flags`.\n+fn param_flag_names(flags: u32) -\u003e Vec\u003c\u0026'static str\u003e {\n+    const IN: u32 = 1 \u003c\u003c 0;\n+    const OUT: u32 = 1 \u003c\u003c 1;\n+    const OTHER: [(u32, \u0026str); 6] = [\n+        (1 \u003c\u003c 3, \"OPTIONAL\"),\n+        (1 \u003c\u003c 4, \"CONST\"),\n+        (1 \u003c\u003c 5, \"VOLATILE\"),\n+        (1 \u003c\u003c 6, \"USER\"),\n+        (1 \u003c\u003c 7, \"DMA\"),\n+        (1 \u003c\u003c 8, \"ALIGNED\"),\n+    ];\n+\n+    let mut names = Vec::new();\n+    if flags \u0026 (IN | OUT) == IN | OUT {\n+        names.push(\"INOUT\");\n+    } else if flags \u0026 IN != 0 {\n+        names.push(\"IN\");\n+    } else if flags \u0026 OUT != 0 {\n+        names.push(\"OUT\");\n+    }\n+    names.extend(\n+        OTHER\n+            .iter()\n+            .filter(|(bit, _)| flags \u0026 bit != 0)\n+            .map(|(_, name)| *name),\n+    );\n+    names\n+}\n+\n+/// Text for a parameter's bounds; either end may be unset.\n+fn range_text(min: Option\u003ci64\u003e, max: Option\u003ci64\u003e) -\u003e Option\u003cString\u003e {\n+    match (min, max) {\n+        (Some(min), Some(max)) =\u003e Some(format!(\"{min} to {max}\")),\n+        (Some(min), None) =\u003e Some(format!(\"\u003e= {min}\")),\n+        (None, Some(max)) =\u003e Some(format!(\"\u003c= {max}\")),\n+        (None, None) =\u003e None,\n+    }\n+}\n+\n+/// Write `text` line by line, indenting every non-blank line by `indent`.\n+fn write_indented(w: \u0026mut dyn Write, indent: \u0026str, text: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+    for line in text.lines() {\n+        if line.is_empty() {\n+            writeln!(w)?;\n+        } else {\n+            writeln!(w, \"{indent}{line}\")?;\n+        }\n+    }\n+    Ok(())\n+}\n+\n+/// Prepare text with embedded newlines for reStructuredText. Wrapped\n+/// prose stays in its paragraph, but a bullet list needs a blank line\n+/// before its first item and after its last one to be recognised.\n+fn rst_text(text: \u0026str) -\u003e String {\n+    let mut out: Vec\u003c\u0026str\u003e = Vec::new();\n+    let mut prev_bullet = false;\n+    for line in text.lines() {\n+        let bullet = line.starts_with(\"- \");\n+        if !line.is_empty() \u0026\u0026 bullet != prev_bullet \u0026\u0026 out.last().is_some_and(|l| !l.is_empty()) {\n+            out.push(\"\");\n+        }\n+        out.push(line);\n+        if !line.is_empty() {\n+            prev_bullet = bullet;\n+        }\n+    }\n+    out.join(\"\\n\")\n+}\n+\n+pub trait OutputFormatter {\n+    fn begin_document(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_document(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_api_list(\u0026mut self, w: \u0026mut dyn Write, title: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+    fn api_item(\u0026mut self, w: \u0026mut dyn Write, name: \u0026str, api_type: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_api_list(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn total_specs(\u0026mut self, w: \u0026mut dyn Write, count: usize) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_api_details(\u0026mut self, w: \u0026mut dyn Write, name: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_api_details(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+    fn description(\u0026mut self, w: \u0026mut dyn Write, desc: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+    fn long_description(\u0026mut self, w: \u0026mut dyn Write, desc: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_context_flags(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+    fn context_flag(\u0026mut self, w: \u0026mut dyn Write, flag: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_context_flags(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_parameters(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn parameter(\u0026mut self, w: \u0026mut dyn Write, param: \u0026ParamSpec) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_parameters(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn return_spec(\u0026mut self, w: \u0026mut dyn Write, ret: \u0026ReturnSpec) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_errors(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn error(\u0026mut self, w: \u0026mut dyn Write, error: \u0026ErrorSpec) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_errors(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn examples(\u0026mut self, w: \u0026mut dyn Write, examples: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+    fn notes(\u0026mut self, w: \u0026mut dyn Write, notes: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+\n+    // Sysfs-specific methods\n+    fn sysfs_subsystem(\u0026mut self, w: \u0026mut dyn Write, subsystem: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+    fn sysfs_path(\u0026mut self, w: \u0026mut dyn Write, path: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+    fn sysfs_permissions(\u0026mut self, w: \u0026mut dyn Write, perms: \u0026str) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_capabilities(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+    fn capability(\u0026mut self, w: \u0026mut dyn Write, cap: \u0026CapabilitySpec) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_capabilities(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    // Signal-related methods\n+    fn begin_signals(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn signal(\u0026mut self, w: \u0026mut dyn Write, signal: \u0026SignalSpec) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_signals(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_signal_masks(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn signal_mask(\u0026mut self, w: \u0026mut dyn Write, mask: \u0026SignalMaskSpec) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_signal_masks(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    // Side effects and state transitions\n+    fn begin_side_effects(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn side_effect(\u0026mut self, w: \u0026mut dyn Write, effect: \u0026SideEffectSpec) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_side_effects(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_state_transitions(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn state_transition(\n+        \u0026mut self,\n+        w: \u0026mut dyn Write,\n+        trans: \u0026StateTransitionSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_state_transitions(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    // Constraints and locks\n+    fn begin_constraints(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn constraint(\u0026mut self, w: \u0026mut dyn Write, constraint: \u0026ConstraintSpec)\n+        -\u003e std::io::Result\u003c()\u003e;\n+    fn end_constraints(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_locks(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn lock(\u0026mut self, w: \u0026mut dyn Write, lock: \u0026LockSpec) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_locks(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+\n+    fn begin_struct_specs(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e;\n+    fn struct_spec(\u0026mut self, w: \u0026mut dyn Write, spec: \u0026StructSpec) -\u003e std::io::Result\u003c()\u003e;\n+    fn end_struct_specs(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e;\n+}\n+\n+pub fn create_formatter(format: OutputFormat) -\u003e Box\u003cdyn OutputFormatter\u003e {\n+    match format {\n+        OutputFormat::Plain =\u003e Box::new(PlainFormatter::new()),\n+        OutputFormat::Json =\u003e Box::new(JsonFormatter::new()),\n+        OutputFormat::Rst =\u003e Box::new(RstFormatter::new()),\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+\n+    #[test]\n+    fn param_flags_use_the_header_bit_values() {\n+        assert!(param_flag_names(0).is_empty());\n+        assert_eq!(param_flag_names(0x1), [\"IN\"]);\n+        assert_eq!(param_flag_names(0x2), [\"OUT\"]);\n+        assert_eq!(param_flag_names(0x3), [\"INOUT\"]);\n+        assert_eq!(param_flag_names(0x42), [\"OUT\", \"USER\"]);\n+        assert_eq!(param_flag_names(0x9), [\"IN\", \"OPTIONAL\"]);\n+        assert_eq!(\n+            param_flag_names(0x1f8),\n+            [\"OPTIONAL\", \"CONST\", \"VOLATILE\", \"USER\", \"DMA\", \"ALIGNED\"]\n+        );\n+    }\n+\n+    #[test]\n+    fn range_text_shows_one_sided_bounds() {\n+        assert_eq!(range_text(Some(0), Some(9)).as_deref(), Some(\"0 to 9\"));\n+        assert_eq!(range_text(Some(1), None).as_deref(), Some(\"\u003e= 1\"));\n+        assert_eq!(range_text(None, Some(255)).as_deref(), Some(\"\u003c= 255\"));\n+        assert_eq!(range_text(None, None), None);\n+    }\n+}\ndiff --git a/tools/kapi/src/formatter/plain.rs b/tools/kapi/src/formatter/plain.rs\nnew file mode 100644\nindex 0000000000000..0aa03ab8573b6\n--- /dev/null\n+++ b/tools/kapi/src/formatter/plain.rs\n@@ -0,0 +1,679 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use super::OutputFormatter;\n+use crate::extractor::{\n+    CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec, SideEffectSpec,\n+    SignalMaskSpec, SignalSpec, StateTransitionSpec,\n+};\n+use std::io::Write;\n+\n+pub struct PlainFormatter;\n+\n+impl PlainFormatter {\n+    pub fn new() -\u003e Self {\n+        PlainFormatter\n+    }\n+}\n+\n+impl OutputFormatter for PlainFormatter {\n+    fn begin_document(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn end_document(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_api_list(\u0026mut self, w: \u0026mut dyn Write, title: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\n{title}:\")?;\n+        writeln!(w, \"{}\", \"-\".repeat(title.len() + 1))\n+    }\n+\n+    fn api_item(\u0026mut self, w: \u0026mut dyn Write, name: \u0026str, _api_type: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"  {name}\")\n+    }\n+\n+    fn end_api_list(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn total_specs(\u0026mut self, w: \u0026mut dyn Write, count: usize) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nTotal specifications found: {count}\")\n+    }\n+\n+    fn begin_api_details(\u0026mut self, w: \u0026mut dyn Write, name: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nDetailed information for {name}:\")?;\n+        writeln!(w, \"{}=\", \"=\".repeat(25 + name.len()))\n+    }\n+\n+    fn end_api_details(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn description(\u0026mut self, w: \u0026mut dyn Write, desc: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"Description: {desc}\")\n+    }\n+\n+    fn long_description(\u0026mut self, w: \u0026mut dyn Write, desc: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nDetailed Description:\")?;\n+        super::write_indented(w, \"  \", desc)\n+    }\n+\n+    fn begin_context_flags(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nExecution Context:\")\n+    }\n+\n+    fn context_flag(\u0026mut self, w: \u0026mut dyn Write, flag: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"  - {flag}\")\n+    }\n+\n+    fn end_context_flags(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_parameters(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nParameters ({count}):\")\n+    }\n+\n+    fn parameter(\u0026mut self, w: \u0026mut dyn Write, param: \u0026ParamSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(\n+            w,\n+            \"  [{}] {} ({})\",\n+            param.index, param.name, param.type_name\n+        )?;\n+        if !param.description.is_empty() {\n+            writeln!(w, \"      {}\", param.description)?;\n+        }\n+\n+        // Display flags\n+        let flags = super::param_flag_names(param.flags);\n+        if !flags.is_empty() {\n+            writeln!(w, \"      Flags: {}\", flags.join(\" | \"))?;\n+        }\n+\n+        // Display constraints\n+        if let Some(constraint) = \u0026param.constraint {\n+            writeln!(w, \"      Constraint: {constraint}\")?;\n+        }\n+        if let Some(range) = super::range_text(param.min_value, param.max_value) {\n+            writeln!(w, \"      Range: {range}\")?;\n+        }\n+        if let Some(mask) = param.valid_mask {\n+            writeln!(w, \"      Valid mask: 0x{mask:x}\")?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_parameters(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn return_spec(\u0026mut self, w: \u0026mut dyn Write, ret: \u0026ReturnSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nReturn Value:\")?;\n+        writeln!(w, \"  Type: {}\", ret.type_name)?;\n+        writeln!(w, \"  {}\", ret.description)?;\n+        if let Some(val) = ret.success_value {\n+            writeln!(w, \"  Success value: {val}\")?;\n+        }\n+        if let (Some(min), Some(max)) = (ret.success_min, ret.success_max) {\n+            writeln!(w, \"  Success range: {min} to {max}\")?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn begin_errors(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nPossible Errors ({count}):\")\n+    }\n+\n+    fn error(\u0026mut self, w: \u0026mut dyn Write, error: \u0026ErrorSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"  {} ({})\", error.name, error.error_code)?;\n+        if !error.condition.is_empty() {\n+            writeln!(w, \"      Condition: {}\", error.condition)?;\n+        }\n+        if !error.description.is_empty() {\n+            writeln!(w, \"      {}\", error.description)?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_errors(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn examples(\u0026mut self, w: \u0026mut dyn Write, examples: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nExamples:\")?;\n+        super::write_indented(w, \"  \", examples)\n+    }\n+\n+    fn notes(\u0026mut self, w: \u0026mut dyn Write, notes: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nNotes:\")?;\n+        super::write_indented(w, \"  \", notes)\n+    }\n+\n+    fn sysfs_subsystem(\u0026mut self, w: \u0026mut dyn Write, subsystem: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"Subsystem: {subsystem}\")\n+    }\n+\n+    fn sysfs_path(\u0026mut self, w: \u0026mut dyn Write, path: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"Sysfs Path: {path}\")\n+    }\n+\n+    fn sysfs_permissions(\u0026mut self, w: \u0026mut dyn Write, perms: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"Permissions: {perms}\")\n+    }\n+\n+    fn begin_capabilities(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nRequired Capabilities:\")\n+    }\n+\n+    fn capability(\u0026mut self, w: \u0026mut dyn Write, cap: \u0026CapabilitySpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"  {} ({}) - {}\", cap.name, cap.capability, cap.action)?;\n+        if !cap.allows.is_empty() {\n+            writeln!(w, \"    Allows: {}\", cap.allows)?;\n+        }\n+        if !cap.without_cap.is_empty() {\n+            writeln!(w, \"    Without capability: {}\", cap.without_cap)?;\n+        }\n+        if let Some(cond) = \u0026cap.check_condition {\n+            writeln!(w, \"    Condition: {cond}\")?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_capabilities(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    // Signal-related methods\n+    fn begin_signals(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nSignal Specifications ({count}):\")\n+    }\n+\n+    fn signal(\u0026mut self, w: \u0026mut dyn Write, signal: \u0026SignalSpec) -\u003e std::io::Result\u003c()\u003e {\n+        write!(w, \"  {} ({})\", signal.signal_name, signal.signal_num)?;\n+\n+        // Display direction (bitmask matching C enum kapi_signal_direction)\n+        let mut dirs = Vec::new();\n+        if signal.direction \u0026 1 != 0 {\n+            dirs.push(\"RECEIVE\");\n+        }\n+        if signal.direction \u0026 2 != 0 {\n+            dirs.push(\"SEND\");\n+        }\n+        if signal.direction \u0026 4 != 0 {\n+            dirs.push(\"HANDLE\");\n+        }\n+        if signal.direction \u0026 8 != 0 {\n+            dirs.push(\"BLOCK\");\n+        }\n+        if signal.direction \u0026 16 != 0 {\n+            dirs.push(\"IGNORE\");\n+        }\n+        let direction = if dirs.is_empty() {\n+            \"UNKNOWN\".to_string()\n+        } else {\n+            dirs.join(\"|\")\n+        };\n+        write!(w, \" - {direction}\")?;\n+\n+        // Display action (matching C enum kapi_signal_action)\n+        let action = match signal.action {\n+            0 =\u003e \"DEFAULT\",\n+            1 =\u003e \"TERMINATE\",\n+            2 =\u003e \"COREDUMP\",\n+            3 =\u003e \"STOP\",\n+            4 =\u003e \"CONTINUE\",\n+            5 =\u003e \"CUSTOM\",\n+            6 =\u003e \"RETURN\",\n+            7 =\u003e \"RESTART\",\n+            8 =\u003e \"QUEUE\",\n+            9 =\u003e \"DISCARD\",\n+            10 =\u003e \"TRANSFORM\",\n+            _ =\u003e \"UNKNOWN\",\n+        };\n+        writeln!(w, \" - {action}\")?;\n+\n+        if let Some(target) = \u0026signal.target {\n+            writeln!(w, \"      Target: {target}\")?;\n+        }\n+        if let Some(condition) = \u0026signal.condition {\n+            writeln!(w, \"      Condition: {condition}\")?;\n+        }\n+        if let Some(desc) = \u0026signal.description {\n+            writeln!(w, \"      {desc}\")?;\n+        }\n+\n+        // Display timing\n+        let timing = match signal.timing {\n+            0 =\u003e \"BEFORE\",\n+            1 =\u003e \"DURING\",\n+            2 =\u003e \"AFTER\",\n+            3 =\u003e \"EXIT\",\n+            _ =\u003e \"UNKNOWN\",\n+        };\n+        writeln!(w, \"      Timing: {timing}\")?;\n+        writeln!(w, \"      Priority: {}\", signal.priority)?;\n+\n+        if signal.restartable {\n+            writeln!(w, \"      Restartable: yes\")?;\n+        }\n+        if signal.interruptible {\n+            writeln!(w, \"      Interruptible: yes\")?;\n+        }\n+        if let Some(queue) = \u0026signal.queue {\n+            writeln!(w, \"      Queue: {queue}\")?;\n+        }\n+        if signal.sa_flags_required != 0 {\n+            writeln!(\n+                w,\n+                \"      SA flags required: {:#x}\",\n+                signal.sa_flags_required\n+            )?;\n+        }\n+        if signal.sa_flags_forbidden != 0 {\n+            writeln!(\n+                w,\n+                \"      SA flags forbidden: {:#x}\",\n+                signal.sa_flags_forbidden\n+            )?;\n+        }\n+        if signal.state_required != 0 {\n+            writeln!(w, \"      State required: {:#x}\", signal.state_required)?;\n+        }\n+        if signal.state_forbidden != 0 {\n+            writeln!(w, \"      State forbidden: {:#x}\", signal.state_forbidden)?;\n+        }\n+        if let Some(error) = signal.error_on_signal {\n+            writeln!(w, \"      Error on signal: {error}\")?;\n+        }\n+        if let Some(transform) = signal.transform_to {\n+            writeln!(w, \"      Transform to: {transform}\")?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_signals(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_signal_masks(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nSignal Masks ({count}):\")\n+    }\n+\n+    fn signal_mask(\u0026mut self, w: \u0026mut dyn Write, mask: \u0026SignalMaskSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"  {}\", mask.name)?;\n+        if !mask.description.is_empty() {\n+            writeln!(w, \"      {}\", mask.description)?;\n+        }\n+        if !mask.signals.is_empty() {\n+            let signals: Vec\u003cString\u003e = mask.signals.iter().map(i32::to_string).collect();\n+            writeln!(w, \"      Signals: {}\", signals.join(\", \"))?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_signal_masks(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    // Side effects and state transitions\n+    fn begin_side_effects(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nSide Effects ({count}):\")\n+    }\n+\n+    fn side_effect(\u0026mut self, w: \u0026mut dyn Write, effect: \u0026SideEffectSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"  {} - {}\", effect.target, effect.description)?;\n+        if let Some(condition) = \u0026effect.condition {\n+            writeln!(w, \"      Condition: {condition}\")?;\n+        }\n+        if effect.reversible {\n+            writeln!(w, \"      Reversible: yes\")?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_side_effects(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_state_transitions(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nState Transitions ({count}):\")\n+    }\n+\n+    fn state_transition(\n+        \u0026mut self,\n+        w: \u0026mut dyn Write,\n+        trans: \u0026StateTransitionSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(\n+            w,\n+            \"  {} : {} -\u003e {}\",\n+            trans.object, trans.from_state, trans.to_state\n+        )?;\n+        if let Some(condition) = \u0026trans.condition {\n+            writeln!(w, \"      Condition: {condition}\")?;\n+        }\n+        if !trans.description.is_empty() {\n+            writeln!(w, \"      {}\", trans.description)?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_state_transitions(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    // Constraints and locks\n+    fn begin_constraints(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nAdditional Constraints ({count}):\")\n+    }\n+\n+    fn constraint(\n+        \u0026mut self,\n+        w: \u0026mut dyn Write,\n+        constraint: \u0026ConstraintSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"  {}\", constraint.name)?;\n+        if !constraint.description.is_empty() {\n+            writeln!(w, \"      {}\", constraint.description)?;\n+        }\n+        if let Some(expr) = \u0026constraint.expression {\n+            writeln!(w, \"      Expression: {expr}\")?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_constraints(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_locks(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nLocking Requirements ({count}):\")\n+    }\n+\n+    fn lock(\u0026mut self, w: \u0026mut dyn Write, lock: \u0026LockSpec) -\u003e std::io::Result\u003c()\u003e {\n+        write!(w, \"  {}\", lock.lock_name)?;\n+\n+        // Display lock type\n+        let lock_type = match lock.lock_type {\n+            0 =\u003e \"NONE\",\n+            1 =\u003e \"MUTEX\",\n+            2 =\u003e \"SPINLOCK\",\n+            3 =\u003e \"RWLOCK\",\n+            4 =\u003e \"SEQLOCK\",\n+            5 =\u003e \"RCU\",\n+            6 =\u003e \"SEMAPHORE\",\n+            7 =\u003e \"CUSTOM\",\n+            _ =\u003e \"UNKNOWN\",\n+        };\n+        writeln!(w, \" ({lock_type})\")?;\n+\n+        let scope_str = match lock.scope {\n+            0 =\u003e \"acquired and released\",\n+            1 =\u003e \"acquired (not released)\",\n+            2 =\u003e \"released (held on entry)\",\n+            3 =\u003e \"held by caller\",\n+            _ =\u003e \"unknown\",\n+        };\n+        writeln!(w, \"      Scope: {scope_str}\")?;\n+\n+        if !lock.description.is_empty() {\n+            writeln!(w, \"      {}\", lock.description)?;\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_locks(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_struct_specs(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nStructure Specifications ({count}):\")\n+    }\n+\n+    fn struct_spec(\n+        \u0026mut self,\n+        w: \u0026mut dyn Write,\n+        spec: \u0026crate::extractor::StructSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(\n+            w,\n+            \"  {} (size={}, align={}):\",\n+            spec.name, spec.size, spec.alignment\n+        )?;\n+        if !spec.description.is_empty() {\n+            writeln!(w, \"      {}\", spec.description)?;\n+        }\n+\n+        if !spec.fields.is_empty() {\n+            writeln!(w, \"      Fields ({}):\", spec.field_count)?;\n+            for field in \u0026spec.fields {\n+                write!(w, \"        - {} ({}):\", field.name, field.type_name)?;\n+                if !field.description.is_empty() {\n+                    write!(w, \" {}\", field.description)?;\n+                }\n+                writeln!(w)?;\n+\n+                // Show constraints if present\n+                if field.min_value != 0 || field.max_value != 0 {\n+                    writeln!(\n+                        w,\n+                        \"          Range: [{}, {}]\",\n+                        field.min_value, field.max_value\n+                    )?;\n+                }\n+                if field.valid_mask != 0 {\n+                    writeln!(w, \"          Mask: {:#x}\", field.valid_mask)?;\n+                }\n+            }\n+        }\n+        Ok(())\n+    }\n+\n+    fn end_struct_specs(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+    use crate::extractor::{ErrorSpec, ParamSpec, ReturnSpec};\n+\n+    fn render_plain(f: \u0026mut PlainFormatter, sink: \u0026mut Vec\u003cu8\u003e) -\u003e String {\n+        f.end_document(sink).unwrap();\n+        String::from_utf8(sink.clone()).unwrap()\n+    }\n+\n+    #[test]\n+    fn plain_api_list() {\n+        let mut f = PlainFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_list(\u0026mut sink, \"System Calls\").unwrap();\n+        f.api_item(\u0026mut sink, \"sys_open\", \"syscall\").unwrap();\n+        f.api_item(\u0026mut sink, \"sys_read\", \"syscall\").unwrap();\n+        f.end_api_list(\u0026mut sink).unwrap();\n+        f.total_specs(\u0026mut sink, 2).unwrap();\n+\n+        let out = render_plain(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"sys_open\"));\n+        assert!(out.contains(\"sys_read\"));\n+        assert!(out.contains(\"Total specifications found: 2\"));\n+    }\n+\n+    #[test]\n+    fn plain_api_details() {\n+        let mut f = PlainFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.description(\u0026mut sink, \"A test syscall\").unwrap();\n+        f.long_description(\u0026mut sink, \"Detailed description here\")\n+            .unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_plain(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"sys_test\"));\n+        assert!(out.contains(\"A test syscall\"));\n+        assert!(out.contains(\"Detailed description here\"));\n+    }\n+\n+    #[test]\n+    fn plain_parameters() {\n+        let mut f = PlainFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_write\").unwrap();\n+        f.begin_parameters(\u0026mut sink, 1).unwrap();\n+        f.parameter(\n+            \u0026mut sink,\n+            \u0026ParamSpec {\n+                index: 0,\n+                name: \"fd\".to_string(),\n+                type_name: \"unsigned int\".to_string(),\n+                description: \"file descriptor\".to_string(),\n+                flags: 1,\n+                param_type: 2,\n+                constraint_type: 0,\n+                constraint: None,\n+                min_value: None,\n+                max_value: None,\n+                valid_mask: None,\n+                enum_values: vec![],\n+                size: None,\n+                alignment: None,\n+                size_param_idx: None,\n+            },\n+        )\n+        .unwrap();\n+        f.end_parameters(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_plain(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"fd\"));\n+        assert!(out.contains(\"unsigned int\"));\n+        assert!(out.contains(\"file descriptor\"));\n+    }\n+\n+    #[test]\n+    fn plain_errors() {\n+        let mut f = PlainFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.begin_errors(\u0026mut sink, 1).unwrap();\n+        f.error(\n+            \u0026mut sink,\n+            \u0026ErrorSpec {\n+                error_code: -2,\n+                name: \"ENOENT\".to_string(),\n+                condition: \"File not found\".to_string(),\n+                description: \"The file does not exist\".to_string(),\n+            },\n+        )\n+        .unwrap();\n+        f.end_errors(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_plain(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"ENOENT\"));\n+        assert!(out.contains(\"-2\"));\n+        assert!(out.contains(\"File not found\"));\n+    }\n+\n+    #[test]\n+    fn plain_return_spec() {\n+        let mut f = PlainFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.return_spec(\n+            \u0026mut sink,\n+            \u0026ReturnSpec {\n+                type_name: \"KAPI_TYPE_INT\".to_string(),\n+                description: \"Returns 0 on success\".to_string(),\n+                return_type: 1,\n+                check_type: 0,\n+                success_value: Some(0),\n+                success_min: None,\n+                success_max: None,\n+                error_values: vec![],\n+            },\n+        )\n+        .unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_plain(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"KAPI_TYPE_INT\"));\n+        assert!(out.contains(\"Returns 0 on success\"));\n+    }\n+\n+    #[test]\n+    fn plain_context_flags() {\n+        let mut f = PlainFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.begin_context_flags(\u0026mut sink).unwrap();\n+        f.context_flag(\u0026mut sink, \"KAPI_CTX_PROCESS\").unwrap();\n+        f.context_flag(\u0026mut sink, \"KAPI_CTX_SLEEPABLE\").unwrap();\n+        f.end_context_flags(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_plain(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"KAPI_CTX_PROCESS\"));\n+        assert!(out.contains(\"KAPI_CTX_SLEEPABLE\"));\n+    }\n+\n+    #[test]\n+    fn plain_signal_mask_lists_its_signals() {\n+        let mut f = PlainFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.begin_signal_masks(\u0026mut sink, 1).unwrap();\n+        f.signal_mask(\n+            \u0026mut sink,\n+            \u0026SignalMaskSpec {\n+                name: \"blocked\".to_string(),\n+                description: \"Blocked while running\".to_string(),\n+                signals: vec![2, 15],\n+            },\n+        )\n+        .unwrap();\n+        f.end_signal_masks(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_plain(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"  blocked\\n      Blocked while running\\n      Signals: 2, 15\\n\"));\n+    }\n+\n+    #[test]\n+    fn plain_multiline_blocks_are_indented() {\n+        let mut f = PlainFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.long_description(\u0026mut sink, \"First paragraph.\\n\\n- item one\\n- item two\")\n+            .unwrap();\n+        f.examples(\u0026mut sink, \"a();\\nif (x) {\\n  b();\\n}\").unwrap();\n+        f.notes(\u0026mut sink, \"Note one.\\n\\nNote two.\").unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_plain(\u0026mut f, \u0026mut sink);\n+        assert!(out\n+            .contains(\"Detailed Description:\\n  First paragraph.\\n\\n  - item one\\n  - item two\\n\"));\n+        assert!(out.contains(\"Examples:\\n  a();\\n  if (x) {\\n    b();\\n  }\\n\"));\n+        assert!(out.contains(\"Notes:\\n  Note one.\\n\\n  Note two.\\n\"));\n+    }\n+}\ndiff --git a/tools/kapi/src/formatter/rst.rs b/tools/kapi/src/formatter/rst.rs\nnew file mode 100644\nindex 0000000000000..c208d876c9777\n--- /dev/null\n+++ b/tools/kapi/src/formatter/rst.rs\n@@ -0,0 +1,802 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+use super::OutputFormatter;\n+use crate::extractor::{\n+    CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec, SideEffectSpec,\n+    SignalMaskSpec, SignalSpec, StateTransitionSpec,\n+};\n+use std::io::Write;\n+\n+pub struct RstFormatter;\n+\n+impl RstFormatter {\n+    pub fn new() -\u003e Self {\n+        RstFormatter\n+    }\n+\n+    fn section_char(level: usize) -\u003e char {\n+        match level {\n+            0 =\u003e '=',\n+            1 =\u003e '-',\n+            2 =\u003e '~',\n+            3 =\u003e '^',\n+            _ =\u003e '\"',\n+        }\n+    }\n+}\n+\n+impl OutputFormatter for RstFormatter {\n+    fn begin_document(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn end_document(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_api_list(\u0026mut self, w: \u0026mut dyn Write, title: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\n{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(0).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn api_item(\u0026mut self, w: \u0026mut dyn Write, name: \u0026str, api_type: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"* **{name}** (*{api_type}*)\")\n+    }\n+\n+    fn end_api_list(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn total_specs(\u0026mut self, w: \u0026mut dyn Write, count: usize) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\n**Total specifications found:** {count}\")\n+    }\n+\n+    fn begin_api_details(\u0026mut self, w: \u0026mut dyn Write, name: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\n{name}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(0).to_string().repeat(name.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn end_api_details(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn description(\u0026mut self, w: \u0026mut dyn Write, desc: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"**{desc}**\")?;\n+        writeln!(w)\n+    }\n+\n+    fn long_description(\u0026mut self, w: \u0026mut dyn Write, desc: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"{}\", super::rst_text(desc))?;\n+        writeln!(w)\n+    }\n+\n+    fn begin_context_flags(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        let title = \"Execution Context\";\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn context_flag(\u0026mut self, w: \u0026mut dyn Write, flag: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"* {flag}\")\n+    }\n+\n+    fn end_context_flags(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w)\n+    }\n+\n+    fn begin_parameters(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        let title = format!(\"Parameters ({count})\");\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn end_parameters(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_errors(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        let title = format!(\"Possible Errors ({count})\");\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn end_errors(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn examples(\u0026mut self, w: \u0026mut dyn Write, examples: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        let title = \"Examples\";\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)?;\n+        writeln!(w, \".. code-block:: c\")?;\n+        writeln!(w)?;\n+        super::write_indented(w, \"   \", examples)?;\n+        writeln!(w)\n+    }\n+\n+    fn notes(\u0026mut self, w: \u0026mut dyn Write, notes: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        let title = \"Notes\";\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)?;\n+        writeln!(w, \"{}\", super::rst_text(notes))?;\n+        writeln!(w)\n+    }\n+\n+    fn sysfs_subsystem(\u0026mut self, w: \u0026mut dyn Write, subsystem: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \":Subsystem: {subsystem}\")?;\n+        writeln!(w)\n+    }\n+\n+    fn sysfs_path(\u0026mut self, w: \u0026mut dyn Write, path: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \":Sysfs Path: {path}\")?;\n+        writeln!(w)\n+    }\n+\n+    fn sysfs_permissions(\u0026mut self, w: \u0026mut dyn Write, perms: \u0026str) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \":Permissions: {perms}\")?;\n+        writeln!(w)\n+    }\n+\n+    fn begin_capabilities(\u0026mut self, w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        let title = \"Required Capabilities\";\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn capability(\u0026mut self, w: \u0026mut dyn Write, cap: \u0026CapabilitySpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"**{} ({})** - {}\", cap.name, cap.capability, cap.action)?;\n+        writeln!(w)?;\n+        if !cap.allows.is_empty() {\n+            writeln!(w, \"* **Allows:** {}\", cap.allows)?;\n+        }\n+        if !cap.without_cap.is_empty() {\n+            writeln!(w, \"* **Without capability:** {}\", cap.without_cap)?;\n+        }\n+        if let Some(cond) = \u0026cap.check_condition {\n+            writeln!(w, \"* **Condition:** {}\", cond)?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn end_capabilities(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn parameter(\u0026mut self, w: \u0026mut dyn Write, param: \u0026ParamSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(\n+            w,\n+            \"**[{}] {}** (*{}*)\",\n+            param.index, param.name, param.type_name\n+        )?;\n+        writeln!(w)?;\n+        writeln!(w, \"  {}\", param.description)?;\n+\n+        // Display flags\n+        let flags = super::param_flag_names(param.flags);\n+        if !flags.is_empty() {\n+            writeln!(w, \"  :Flags: {}\", flags.join(\", \"))?;\n+        }\n+\n+        if let Some(constraint) = \u0026param.constraint {\n+            writeln!(w, \"  :Constraint: {}\", constraint)?;\n+        }\n+\n+        if let Some(range) = super::range_text(param.min_value, param.max_value) {\n+            writeln!(w, \"  :Range: {}\", range)?;\n+        }\n+\n+        writeln!(w)\n+    }\n+\n+    fn return_spec(\u0026mut self, w: \u0026mut dyn Write, ret: \u0026ReturnSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"\\nReturn Value\")?;\n+        writeln!(w, \"{}\\n\", Self::section_char(1).to_string().repeat(12))?;\n+        writeln!(w)?;\n+        writeln!(w, \":Type: {}\", ret.type_name)?;\n+        writeln!(w, \":Description: {}\", ret.description)?;\n+        if let Some(success) = ret.success_value {\n+            writeln!(w, \":Success value: {}\", success)?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn error(\u0026mut self, w: \u0026mut dyn Write, error: \u0026ErrorSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"**{}** ({})\", error.name, error.error_code)?;\n+        writeln!(w)?;\n+        writeln!(w, \"  :Condition: {}\", error.condition)?;\n+        if !error.description.is_empty() {\n+            writeln!(w, \"  :Description: {}\", error.description)?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn begin_signals(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        let title = format!(\"Signals ({count})\");\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn signal(\u0026mut self, w: \u0026mut dyn Write, signal: \u0026SignalSpec) -\u003e std::io::Result\u003c()\u003e {\n+        write!(w, \"* **{}**\", signal.signal_name)?;\n+        if signal.signal_num != 0 {\n+            write!(w, \" ({})\", signal.signal_num)?;\n+        }\n+        writeln!(w)?;\n+\n+        // Direction (bitmask matching C enum kapi_signal_direction)\n+        let mut dirs = Vec::new();\n+        if signal.direction \u0026 1 != 0 {\n+            dirs.push(\"receive\");\n+        }\n+        if signal.direction \u0026 2 != 0 {\n+            dirs.push(\"send\");\n+        }\n+        if signal.direction \u0026 4 != 0 {\n+            dirs.push(\"handle\");\n+        }\n+        if signal.direction \u0026 8 != 0 {\n+            dirs.push(\"block\");\n+        }\n+        if signal.direction \u0026 16 != 0 {\n+            dirs.push(\"ignore\");\n+        }\n+        let direction = if dirs.is_empty() {\n+            \"unknown\".to_string()\n+        } else {\n+            dirs.join(\", \")\n+        };\n+        writeln!(w, \"  :Direction: {}\", direction)?;\n+\n+        // Action (matching C enum kapi_signal_action)\n+        let action = match signal.action {\n+            0 =\u003e \"default\",\n+            1 =\u003e \"terminate\",\n+            2 =\u003e \"coredump\",\n+            3 =\u003e \"stop\",\n+            4 =\u003e \"continue\",\n+            5 =\u003e \"custom\",\n+            6 =\u003e \"return\",\n+            7 =\u003e \"restart\",\n+            8 =\u003e \"queue\",\n+            9 =\u003e \"discard\",\n+            10 =\u003e \"transform\",\n+            _ =\u003e \"unknown\",\n+        };\n+        writeln!(w, \"  :Action: {}\", action)?;\n+\n+        if let Some(target) = \u0026signal.target {\n+            writeln!(w, \"  :Target: {}\", target)?;\n+        }\n+        if let Some(cond) = \u0026signal.condition {\n+            writeln!(w, \"  :Condition: {}\", cond)?;\n+        }\n+        if let Some(desc) = \u0026signal.description {\n+            writeln!(w, \"  :Description: {}\", desc)?;\n+        }\n+        let timing = match signal.timing {\n+            0 =\u003e \"before\",\n+            1 =\u003e \"during\",\n+            2 =\u003e \"after\",\n+            3 =\u003e \"exit\",\n+            _ =\u003e \"\",\n+        };\n+        if !timing.is_empty() {\n+            writeln!(w, \"  :Timing: {}\", timing)?;\n+        }\n+        if signal.priority != 0 {\n+            writeln!(w, \"  :Priority: {}\", signal.priority)?;\n+        }\n+        if signal.interruptible {\n+            writeln!(w, \"  :Interruptible: yes\")?;\n+        }\n+        if signal.restartable {\n+            writeln!(w, \"  :Restartable: yes\")?;\n+        }\n+        if let Some(queue) = \u0026signal.queue {\n+            writeln!(w, \"  :Queue: {}\", queue)?;\n+        }\n+        if signal.sa_flags_required != 0 {\n+            writeln!(w, \"  :SA flags required: {:#x}\", signal.sa_flags_required)?;\n+        }\n+        if signal.sa_flags_forbidden != 0 {\n+            writeln!(w, \"  :SA flags forbidden: {:#x}\", signal.sa_flags_forbidden)?;\n+        }\n+        if signal.state_required != 0 {\n+            writeln!(w, \"  :State required: {:#x}\", signal.state_required)?;\n+        }\n+        if signal.state_forbidden != 0 {\n+            writeln!(w, \"  :State forbidden: {:#x}\", signal.state_forbidden)?;\n+        }\n+        if let Some(error) = signal.error_on_signal {\n+            writeln!(w, \"  :Error on signal: {}\", error)?;\n+        }\n+        if let Some(transform) = signal.transform_to {\n+            writeln!(w, \"  :Transform to: {}\", transform)?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn end_signals(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_signal_masks(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        let title = format!(\"Signal Masks ({count})\");\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn signal_mask(\u0026mut self, w: \u0026mut dyn Write, mask: \u0026SignalMaskSpec) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"* **{}**\", mask.name)?;\n+        if !mask.description.is_empty() {\n+            writeln!(w, \"  {}\", mask.description)?;\n+        }\n+        if !mask.signals.is_empty() {\n+            let signals: Vec\u003cString\u003e = mask.signals.iter().map(i32::to_string).collect();\n+            writeln!(w, \"  Signals: {}\", signals.join(\", \"))?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn end_signal_masks(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_side_effects(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        let title = format!(\"Side Effects ({count})\");\n+        writeln!(w, \"{}\\n\", title)?;\n+        writeln!(\n+            w,\n+            \"{}\\n\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )\n+    }\n+\n+    fn side_effect(\u0026mut self, w: \u0026mut dyn Write, effect: \u0026SideEffectSpec) -\u003e std::io::Result\u003c()\u003e {\n+        write!(w, \"* **{}**\", effect.target)?;\n+        if effect.reversible {\n+            write!(w, \" *(reversible)*\")?;\n+        }\n+        writeln!(w)?;\n+        writeln!(w, \"  {}\", effect.description)?;\n+        if let Some(cond) = \u0026effect.condition {\n+            writeln!(w, \"  :Condition: {}\", cond)?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn end_side_effects(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_state_transitions(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        let title = format!(\"State Transitions ({count})\");\n+        writeln!(w, \"{}\\n\", title)?;\n+        writeln!(\n+            w,\n+            \"{}\\n\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )\n+    }\n+\n+    fn state_transition(\n+        \u0026mut self,\n+        w: \u0026mut dyn Write,\n+        trans: \u0026StateTransitionSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(\n+            w,\n+            \"* **{}**: {} → {}\",\n+            trans.object, trans.from_state, trans.to_state\n+        )?;\n+        writeln!(w, \"  {}\", trans.description)?;\n+        if let Some(cond) = \u0026trans.condition {\n+            writeln!(w, \"  :Condition: {}\", cond)?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn end_state_transitions(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_constraints(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        let title = format!(\"Constraints ({count})\");\n+        writeln!(w, \"{title}\")?;\n+        writeln!(\n+            w,\n+            \"{}\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )?;\n+        writeln!(w)\n+    }\n+\n+    fn constraint(\n+        \u0026mut self,\n+        w: \u0026mut dyn Write,\n+        constraint: \u0026ConstraintSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"* **{}**\", constraint.name)?;\n+        if !constraint.description.is_empty() {\n+            writeln!(w, \"  {}\", constraint.description)?;\n+        }\n+        if let Some(expr) = \u0026constraint.expression {\n+            writeln!(w, \"  :Expression: ``{}``\", expr)?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn end_constraints(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_locks(\u0026mut self, w: \u0026mut dyn Write, count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        let title = format!(\"Locks ({count})\");\n+        writeln!(w, \"{}\\n\", title)?;\n+        writeln!(\n+            w,\n+            \"{}\\n\",\n+            Self::section_char(1).to_string().repeat(title.len())\n+        )\n+    }\n+\n+    fn lock(\u0026mut self, w: \u0026mut dyn Write, lock: \u0026LockSpec) -\u003e std::io::Result\u003c()\u003e {\n+        write!(w, \"* **{}**\", lock.lock_name)?;\n+        let lock_type_str = match lock.lock_type {\n+            1 =\u003e \" *(mutex)*\",\n+            2 =\u003e \" *(spinlock)*\",\n+            3 =\u003e \" *(rwlock)*\",\n+            4 =\u003e \" *(seqlock)*\",\n+            5 =\u003e \" *(RCU)*\",\n+            6 =\u003e \" *(semaphore)*\",\n+            7 =\u003e \" *(custom)*\",\n+            _ =\u003e \"\",\n+        };\n+        writeln!(w, \"{}\", lock_type_str)?;\n+        if !lock.description.is_empty() {\n+            writeln!(w, \"  {}\", lock.description)?;\n+        }\n+        writeln!(w)\n+    }\n+\n+    fn end_locks(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+\n+    fn begin_struct_specs(\u0026mut self, w: \u0026mut dyn Write, _count: u32) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w)?;\n+        writeln!(w, \"Structure Specifications\")?;\n+        writeln!(w, \"~~~~~~~~~~~~~~~~~~~~~~~\")?;\n+        writeln!(w)\n+    }\n+\n+    fn struct_spec(\n+        \u0026mut self,\n+        w: \u0026mut dyn Write,\n+        spec: \u0026crate::extractor::StructSpec,\n+    ) -\u003e std::io::Result\u003c()\u003e {\n+        writeln!(w, \"**{}**\", spec.name)?;\n+        writeln!(w)?;\n+\n+        if !spec.description.is_empty() {\n+            writeln!(w, \"  {}\", spec.description)?;\n+            writeln!(w)?;\n+        }\n+\n+        writeln!(w, \"  :Size: {} bytes\", spec.size)?;\n+        writeln!(w, \"  :Alignment: {} bytes\", spec.alignment)?;\n+        writeln!(w, \"  :Fields: {}\", spec.field_count)?;\n+        writeln!(w)?;\n+\n+        if !spec.fields.is_empty() {\n+            for field in \u0026spec.fields {\n+                writeln!(w, \"  * **{}** ({})\", field.name, field.type_name)?;\n+                if !field.description.is_empty() {\n+                    writeln!(w, \"    {}\", field.description)?;\n+                }\n+                if field.min_value != 0 || field.max_value != 0 {\n+                    writeln!(w, \"    Range: [{}, {}]\", field.min_value, field.max_value)?;\n+                }\n+            }\n+            writeln!(w)?;\n+        }\n+\n+        Ok(())\n+    }\n+\n+    fn end_struct_specs(\u0026mut self, _w: \u0026mut dyn Write) -\u003e std::io::Result\u003c()\u003e {\n+        Ok(())\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+    use crate::extractor::{ErrorSpec, LockSpec, ParamSpec, ReturnSpec};\n+\n+    fn render_rst(f: \u0026mut RstFormatter, sink: \u0026mut Vec\u003cu8\u003e) -\u003e String {\n+        f.end_document(sink).unwrap();\n+        String::from_utf8(sink.clone()).unwrap()\n+    }\n+\n+    #[test]\n+    fn rst_api_details_has_heading() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.description(\u0026mut sink, \"A test syscall\").unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"sys_test\"));\n+        assert!(out.contains(\"========\"));\n+        assert!(out.contains(\"**A test syscall**\"));\n+    }\n+\n+    #[test]\n+    fn rst_api_list() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_list(\u0026mut sink, \"System Calls\").unwrap();\n+        f.api_item(\u0026mut sink, \"sys_open\", \"syscall\").unwrap();\n+        f.api_item(\u0026mut sink, \"sys_read\", \"syscall\").unwrap();\n+        f.end_api_list(\u0026mut sink).unwrap();\n+        f.total_specs(\u0026mut sink, 2).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"sys_open\"));\n+        assert!(out.contains(\"sys_read\"));\n+    }\n+\n+    #[test]\n+    fn rst_parameters() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_write\").unwrap();\n+        f.begin_parameters(\u0026mut sink, 1).unwrap();\n+        f.parameter(\n+            \u0026mut sink,\n+            \u0026ParamSpec {\n+                index: 0,\n+                name: \"fd\".to_string(),\n+                type_name: \"unsigned int\".to_string(),\n+                description: \"file descriptor\".to_string(),\n+                flags: 1,\n+                param_type: 2,\n+                constraint_type: 0,\n+                constraint: None,\n+                min_value: None,\n+                max_value: None,\n+                valid_mask: None,\n+                enum_values: vec![],\n+                size: None,\n+                alignment: None,\n+                size_param_idx: None,\n+            },\n+        )\n+        .unwrap();\n+        f.end_parameters(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"**[0] fd**\"));\n+        assert!(out.contains(\"unsigned int\"));\n+        assert!(out.contains(\"file descriptor\"));\n+    }\n+\n+    #[test]\n+    fn rst_errors() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.begin_errors(\u0026mut sink, 1).unwrap();\n+        f.error(\n+            \u0026mut sink,\n+            \u0026ErrorSpec {\n+                error_code: -2,\n+                name: \"ENOENT\".to_string(),\n+                condition: \"File not found\".to_string(),\n+                description: \"The file does not exist\".to_string(),\n+            },\n+        )\n+        .unwrap();\n+        f.end_errors(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"**ENOENT**\"));\n+        assert!(out.contains(\"-2\"));\n+        assert!(out.contains(\"File not found\"));\n+    }\n+\n+    #[test]\n+    fn rst_return_spec() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.return_spec(\n+            \u0026mut sink,\n+            \u0026ReturnSpec {\n+                type_name: \"KAPI_TYPE_INT\".to_string(),\n+                description: \"Returns 0 on success\".to_string(),\n+                return_type: 1,\n+                check_type: 0,\n+                success_value: Some(0),\n+                success_min: None,\n+                success_max: None,\n+                error_values: vec![],\n+            },\n+        )\n+        .unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"KAPI_TYPE_INT\"));\n+        assert!(out.contains(\"Returns 0 on success\"));\n+        assert!(out.contains(\"Return Value\"));\n+    }\n+\n+    #[test]\n+    fn rst_context_flags() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.begin_context_flags(\u0026mut sink).unwrap();\n+        f.context_flag(\u0026mut sink, \"KAPI_CTX_PROCESS\").unwrap();\n+        f.context_flag(\u0026mut sink, \"KAPI_CTX_SLEEPABLE\").unwrap();\n+        f.end_context_flags(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"KAPI_CTX_PROCESS\"));\n+        assert!(out.contains(\"KAPI_CTX_SLEEPABLE\"));\n+        assert!(out.contains(\"Execution Context\"));\n+    }\n+\n+    #[test]\n+    fn rst_signal_mask_lists_its_signals() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.begin_signal_masks(\u0026mut sink, 1).unwrap();\n+        f.signal_mask(\n+            \u0026mut sink,\n+            \u0026SignalMaskSpec {\n+                name: \"blocked\".to_string(),\n+                description: \"Blocked while running\".to_string(),\n+                signals: vec![2, 15],\n+            },\n+        )\n+        .unwrap();\n+        f.end_signal_masks(\u0026mut sink).unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"* **blocked**\\n  Blocked while running\\n  Signals: 2, 15\\n\"));\n+    }\n+\n+    #[test]\n+    fn rst_examples_are_a_literal_block_with_blank_lines_kept() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.examples(\u0026mut sink, \"a();\\n  b();\\n\\nc();\").unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\".. code-block:: c\\n\\n   a();\\n     b();\\n\\n   c();\\n\"));\n+    }\n+\n+    #[test]\n+    fn rst_bullets_get_blank_lines_around_the_list() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        f.begin_document(\u0026mut sink).unwrap();\n+        f.begin_api_details(\u0026mut sink, \"sys_test\").unwrap();\n+        f.long_description(\u0026mut sink, \"Intro:\\n- one\\n- two\\n\\nTail.\")\n+            .unwrap();\n+        f.notes(\u0026mut sink, \"Para one.\\n\\n- a\\n- b\\nafter\").unwrap();\n+        f.end_api_details(\u0026mut sink).unwrap();\n+\n+        let out = render_rst(\u0026mut f, \u0026mut sink);\n+        assert!(out.contains(\"Intro:\\n\\n- one\\n- two\\n\\nTail.\\n\"));\n+        assert!(out.contains(\"Para one.\\n\\n- a\\n- b\\n\\nafter\\n\"));\n+    }\n+\n+    #[test]\n+    fn rst_lock_types_follow_the_kernel_enum() {\n+        let mut f = RstFormatter::new();\n+        let mut sink = Vec::new();\n+\n+        for (lock_type, label) in [\n+            (1, \"mutex\"),\n+            (2, \"spinlock\"),\n+            (3, \"rwlock\"),\n+            (4, \"seqlock\"),\n+            (5, \"RCU\"),\n+            (6, \"semaphore\"),\n+            (7, \"custom\"),\n+        ] {\n+            f.lock(\n+                \u0026mut sink,\n+                \u0026LockSpec {\n+                    lock_name: \"l\".to_string(),\n+                    lock_type,\n+                    scope: 0,\n+                    description: String::new(),\n+                },\n+            )\n+            .unwrap();\n+            let out = String::from_utf8(std::mem::take(\u0026mut sink)).unwrap();\n+            assert_eq!(out, format!(\"* **l** *({label})*\\n\\n\"));\n+        }\n+    }\n+}\ndiff --git a/tools/kapi/src/main.rs b/tools/kapi/src/main.rs\nnew file mode 100644\nindex 0000000000000..93ed11b508e9f\n--- /dev/null\n+++ b/tools/kapi/src/main.rs\n@@ -0,0 +1,123 @@\n+// SPDX-License-Identifier: GPL-2.0\n+// Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+//! kapi - Kernel API Specification Tool\n+//!\n+//! This tool extracts and displays kernel API specifications from multiple sources:\n+//! - Kernel source code (kerneldoc blocks)\n+//! - Compiled vmlinux binaries (`.kapi_specs` ELF section)\n+//! - Running kernel via debugfs\n+\n+use anyhow::Result;\n+use clap::Parser;\n+use std::io::{self, Write};\n+\n+mod extractor;\n+mod formatter;\n+\n+use extractor::{ApiExtractor, DebugfsExtractor, SourceExtractor, VmlinuxExtractor};\n+use formatter::{create_formatter, OutputFormat};\n+\n+#[derive(Parser, Debug)]\n+#[command(author, version, about, long_about = None)]\n+struct Args {\n+    /// Path to the vmlinux file\n+    #[arg(long, value_name = \"PATH\", group = \"input\")]\n+    vmlinux: Option\u003cString\u003e,\n+\n+    /// Path to kernel source directory or file\n+    #[arg(long, value_name = \"PATH\", group = \"input\")]\n+    source: Option\u003cString\u003e,\n+\n+    /// Path to debugfs (defaults to /sys/kernel/debug if not specified)\n+    #[arg(long, value_name = \"PATH\", group = \"input\")]\n+    debugfs: Option\u003cString\u003e,\n+\n+    /// Optional: Name of specific API to show details for\n+    api_name: Option\u003cString\u003e,\n+\n+    /// Output format\n+    #[arg(long, short = 'f', default_value = \"plain\")]\n+    format: String,\n+}\n+\n+fn main() -\u003e Result\u003c()\u003e {\n+    let args = Args::parse();\n+\n+    let output_format: OutputFormat = args\n+        .format\n+        .parse()\n+        .map_err(|e: String| anyhow::anyhow!(e))?;\n+\n+    let extractor: Box\u003cdyn ApiExtractor\u003e = match (\u0026args.vmlinux, \u0026args.source, \u0026args.debugfs) {\n+        (Some(vmlinux_path), None, None) =\u003e Box::new(VmlinuxExtractor::new(vmlinux_path)?),\n+        (None, Some(source_path), None) =\u003e Box::new(SourceExtractor::new(source_path)?),\n+        (None, None, Some(_) | None) =\u003e {\n+            // If debugfs is specified or no input is provided, use debugfs\n+            Box::new(DebugfsExtractor::new(args.debugfs.clone())?)\n+        }\n+        _ =\u003e {\n+            anyhow::bail!(\"Please specify only one of --vmlinux, --source, or --debugfs\")\n+        }\n+    };\n+\n+    display_apis(extractor.as_ref(), args.api_name, output_format)\n+}\n+\n+fn display_apis(\n+    extractor: \u0026dyn ApiExtractor,\n+    api_name: Option\u003cString\u003e,\n+    output_format: OutputFormat,\n+) -\u003e Result\u003c()\u003e {\n+    let mut formatter = create_formatter(output_format);\n+    let mut stdout = io::stdout();\n+\n+    formatter.begin_document(\u0026mut stdout)?;\n+\n+    if let Some(api_name_req) = api_name {\n+        // Use the extractor to display API details\n+        if let Some(_spec) = extractor.extract_by_name(\u0026api_name_req)? {\n+            extractor.display_api_details(\u0026api_name_req, \u0026mut *formatter, \u0026mut stdout)?;\n+        } else {\n+            eprintln!(\"API '{}' not found.\", api_name_req);\n+            if output_format == OutputFormat::Plain {\n+                writeln!(stdout, \"\\nAvailable APIs:\")?;\n+                for spec in extractor.extract_all()? {\n+                    writeln!(stdout, \"  {} ({})\", spec.name, spec.api_type)?;\n+                }\n+            }\n+            std::process::exit(1);\n+        }\n+    } else {\n+        // Display list of APIs using the extractor\n+        let all_specs = extractor.extract_all()?;\n+\n+        // Helper to display API list for a specific type\n+        let mut display_api_type = |api_type: \u0026str, title: \u0026str| -\u003e Result\u003c()\u003e {\n+            let filtered: Vec\u003c_\u003e = all_specs\n+                .iter()\n+                .filter(|s| s.api_type == api_type)\n+                .collect();\n+\n+            if !filtered.is_empty() {\n+                formatter.begin_api_list(\u0026mut stdout, title)?;\n+                for spec in filtered {\n+                    formatter.api_item(\u0026mut stdout, \u0026spec.name, \u0026spec.api_type)?;\n+                }\n+                formatter.end_api_list(\u0026mut stdout)?;\n+            }\n+            Ok(())\n+        };\n+\n+        display_api_type(\"syscall\", \"System Calls\")?;\n+        display_api_type(\"ioctl\", \"IOCTLs\")?;\n+        display_api_type(\"function\", \"Functions\")?;\n+        display_api_type(\"sysfs\", \"Sysfs Attributes\")?;\n+\n+        formatter.total_specs(\u0026mut stdout, all_specs.len())?;\n+    }\n+\n+    formatter.end_document(\u0026mut stdout)?;\n+\n+    Ok(())\n+}\ndiff --git a/tools/lib/python/kdoc/kdoc_apispec.py b/tools/lib/python/kdoc/kdoc_apispec.py\nnew file mode 100644\nindex 0000000000000..fb8e243a7b77d\n--- /dev/null\n+++ b/tools/lib/python/kdoc/kdoc_apispec.py\n@@ -0,0 +1,1391 @@\n+#!/usr/bin/env python3\n+# SPDX-License-Identifier: GPL-2.0\n+# Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+\n+\"\"\"\n+Generate C macro invocations for kernel API specifications from kernel-doc comments.\n+\n+This module creates C header files with API specification macros that match\n+the kernel API specification framework in include/linux/kernel_api_spec.h.\n+\"\"\"\n+\n+from kdoc.kdoc_output import OutputFormat\n+import re\n+import sys\n+\n+\n+# Valid KAPI effect types\n+VALID_EFFECT_TYPES = {\n+    'KAPI_EFFECT_NONE', 'KAPI_EFFECT_MODIFY_STATE', 'KAPI_EFFECT_PROCESS_STATE',\n+    'KAPI_EFFECT_IRREVERSIBLE', 'KAPI_EFFECT_SCHEDULE', 'KAPI_EFFECT_FILESYSTEM',\n+    'KAPI_EFFECT_HARDWARE', 'KAPI_EFFECT_ALLOC_MEMORY', 'KAPI_EFFECT_FREE_MEMORY',\n+    'KAPI_EFFECT_SIGNAL_SEND', 'KAPI_EFFECT_FILE_POSITION', 'KAPI_EFFECT_LOCK_ACQUIRE',\n+    'KAPI_EFFECT_LOCK_RELEASE', 'KAPI_EFFECT_RESOURCE_CREATE', 'KAPI_EFFECT_RESOURCE_DESTROY',\n+    'KAPI_EFFECT_NETWORK'\n+}\n+\n+# DSL aliases mapping short tokens to their canonical KAPI_* C\n+# identifier. Unknown tokens pass through unchanged.\n+_CTX_ALIASES = {\n+    'process':          'KAPI_CTX_PROCESS',\n+    'softirq':          'KAPI_CTX_SOFTIRQ',\n+    'hardirq':          'KAPI_CTX_HARDIRQ',\n+    'nmi':              'KAPI_CTX_NMI',\n+    'atomic':           'KAPI_CTX_ATOMIC',\n+    'sleepable':        'KAPI_CTX_SLEEPABLE',\n+    'preempt_disabled': 'KAPI_CTX_PREEMPT_DISABLED',\n+    'irq_disabled':     'KAPI_CTX_IRQ_DISABLED',\n+}\n+_TYPE_ALIASES = {\n+    'int':      'KAPI_TYPE_INT',\n+    'uint':     'KAPI_TYPE_UINT',\n+    'ptr':      'KAPI_TYPE_PTR',\n+    'struct':   'KAPI_TYPE_STRUCT',\n+    'union':    'KAPI_TYPE_UNION',\n+    'enum':     'KAPI_TYPE_ENUM',\n+    'func_ptr': 'KAPI_TYPE_FUNC_PTR',\n+    'array':    'KAPI_TYPE_ARRAY',\n+    'fd':       'KAPI_TYPE_FD',\n+    'user_ptr': 'KAPI_TYPE_USER_PTR',\n+    'uptr':     'KAPI_TYPE_USER_PTR',\n+    'path':     'KAPI_TYPE_PATH',\n+    'custom':   'KAPI_TYPE_CUSTOM',\n+}\n+_FLAG_ALIASES = {\n+    'input':    'KAPI_PARAM_IN',\n+    'in':       'KAPI_PARAM_IN',\n+    'output':   'KAPI_PARAM_OUT',\n+    'out':      'KAPI_PARAM_OUT',\n+    'inout':    'KAPI_PARAM_INOUT',\n+    'optional': 'KAPI_PARAM_OPTIONAL',\n+    'const':    'KAPI_PARAM_CONST',\n+    'volatile': 'KAPI_PARAM_VOLATILE',\n+    'user':     'KAPI_PARAM_USER',\n+    'dma':      'KAPI_PARAM_DMA',\n+    'aligned':  'KAPI_PARAM_ALIGNED',\n+}\n+\n+\n+def _canon_token(tok, table):\n+    \"\"\"Look up `tok` (case-insensitive) in `table`. Unknown tokens\n+    pass through verbatim.\"\"\"\n+    t = tok.strip()\n+    if not t:\n+        return ''\n+    return table.get(t.lower(), t)\n+\n+\n+def _canon_context_expr(expr):\n+    \"\"\"Canonicalise a context flag expression. Accepts '|'- or\n+    ','-joined tokens; returns a '|'-joined string of KAPI_CTX_*\n+    identifiers ready for KAPI_CONTEXT().\"\"\"\n+    if not expr:\n+        return expr\n+    sep = ',' if ',' in expr and '|' not in expr else '|'\n+    tokens = [_canon_token(t, _CTX_ALIASES) for t in expr.split(sep)]\n+    return ' | '.join(t for t in tokens if t)\n+\n+\n+def _canon_flags_expr(expr):\n+    \"\"\"Canonicalise a parameter flags expression. Accepts '|'- or\n+    ','-joined KAPI_PARAM_* tokens or their aliases; returns a\n+    '|'-joined canonical string.\"\"\"\n+    if not expr:\n+        return expr\n+    sep = ',' if ',' in expr and '|' not in expr else '|'\n+    tokens = [_canon_token(t, _FLAG_ALIASES) for t in expr.split(sep)]\n+    return ' | '.join(t for t in tokens if t)\n+\n+\n+# Alias tables for enum families used as block-attribute values\n+# (lock type, signal direction/action/timing, return check type) and\n+# for the top-level `side-effect:` bitmask.\n+_LOCK_TYPE_ALIASES = {\n+    'none':      'KAPI_LOCK_NONE',\n+    'mutex':     'KAPI_LOCK_MUTEX',\n+    'spinlock':  'KAPI_LOCK_SPINLOCK',\n+    'rwlock':    'KAPI_LOCK_RWLOCK',\n+    'seqlock':   'KAPI_LOCK_SEQLOCK',\n+    'rcu':       'KAPI_LOCK_RCU',\n+    'semaphore': 'KAPI_LOCK_SEMAPHORE',\n+    'custom':    'KAPI_LOCK_CUSTOM',\n+}\n+_SIGNAL_DIR_ALIASES = {\n+    'receive': 'KAPI_SIGNAL_RECEIVE',\n+    'send':    'KAPI_SIGNAL_SEND',\n+    'handle':  'KAPI_SIGNAL_HANDLE',\n+    'block':   'KAPI_SIGNAL_BLOCK',\n+    'ignore':  'KAPI_SIGNAL_IGNORE',\n+}\n+_SIGNAL_ACTION_ALIASES = {\n+    'default':   'KAPI_SIGNAL_ACTION_DEFAULT',\n+    'terminate': 'KAPI_SIGNAL_ACTION_TERMINATE',\n+    'coredump':  'KAPI_SIGNAL_ACTION_COREDUMP',\n+    'stop':      'KAPI_SIGNAL_ACTION_STOP',\n+    'continue':  'KAPI_SIGNAL_ACTION_CONTINUE',\n+    'custom':    'KAPI_SIGNAL_ACTION_CUSTOM',\n+    'return':    'KAPI_SIGNAL_ACTION_RETURN',\n+    'restart':   'KAPI_SIGNAL_ACTION_RESTART',\n+    'queue':     'KAPI_SIGNAL_ACTION_QUEUE',\n+    'discard':   'KAPI_SIGNAL_ACTION_DISCARD',\n+    'transform': 'KAPI_SIGNAL_ACTION_TRANSFORM',\n+}\n+_SIGNAL_TIMING_ALIASES = {\n+    'before': 'KAPI_SIGNAL_TIME_BEFORE',\n+    'during': 'KAPI_SIGNAL_TIME_DURING',\n+    'after':  'KAPI_SIGNAL_TIME_AFTER',\n+}\n+_EFFECT_ALIASES = {\n+    'none':             'KAPI_EFFECT_NONE',\n+    'alloc_memory':     'KAPI_EFFECT_ALLOC_MEMORY',\n+    'free_memory':      'KAPI_EFFECT_FREE_MEMORY',\n+    'modify_state':     'KAPI_EFFECT_MODIFY_STATE',\n+    'signal_send':      'KAPI_EFFECT_SIGNAL_SEND',\n+    'file_position':    'KAPI_EFFECT_FILE_POSITION',\n+    'lock_acquire':     'KAPI_EFFECT_LOCK_ACQUIRE',\n+    'lock_release':     'KAPI_EFFECT_LOCK_RELEASE',\n+    'resource_create':  'KAPI_EFFECT_RESOURCE_CREATE',\n+    'resource_destroy': 'KAPI_EFFECT_RESOURCE_DESTROY',\n+    'schedule':         'KAPI_EFFECT_SCHEDULE',\n+    'hardware':         'KAPI_EFFECT_HARDWARE',\n+    'network':          'KAPI_EFFECT_NETWORK',\n+    'filesystem':       'KAPI_EFFECT_FILESYSTEM',\n+    'process_state':    'KAPI_EFFECT_PROCESS_STATE',\n+    'irreversible':     'KAPI_EFFECT_IRREVERSIBLE',\n+}\n+_RETURN_CHECK_ALIASES = {\n+    'exact':       'KAPI_RETURN_EXACT',\n+    'range':       'KAPI_RETURN_RANGE',\n+    'error_check': 'KAPI_RETURN_ERROR_CHECK',\n+    'fd':          'KAPI_RETURN_FD',\n+    'custom':      'KAPI_RETURN_CUSTOM',\n+    'no_return':   'KAPI_RETURN_NO_RETURN',\n+}\n+\n+# Mapping from short architecture name (as used under arch/\u003cname\u003e/) to the\n+# kernel CONFIG_* symbol that selects that architecture. Used by the\n+# `arch-mask:` DSL form to wrap arch-specific mask bits in #ifdef so a\n+# single generated apispec.h compiles on every architecture and folds\n+# the right bits into the mask at compile time.\n+_ARCH_CONFIG = {\n+    'alpha':       'CONFIG_ALPHA',\n+    'arc':         'CONFIG_ARC',\n+    'arm':         'CONFIG_ARM',\n+    'arm64':       'CONFIG_ARM64',\n+    'csky':        'CONFIG_CSKY',\n+    'hexagon':     'CONFIG_HEXAGON',\n+    'loongarch':   'CONFIG_LOONGARCH',\n+    'm68k':        'CONFIG_M68K',\n+    'microblaze':  'CONFIG_MICROBLAZE',\n+    'mips':        'CONFIG_MIPS',\n+    'nios2':       'CONFIG_NIOS2',\n+    'openrisc':    'CONFIG_OPENRISC',\n+    'parisc':      'CONFIG_PARISC',\n+    'powerpc':     'CONFIG_PPC',\n+    'riscv':       'CONFIG_RISCV',\n+    's390':        'CONFIG_S390',\n+    'sh':          'CONFIG_SUPERH',\n+    'sparc':       'CONFIG_SPARC',\n+    'um':          'CONFIG_UML',\n+    'x86':         'CONFIG_X86',\n+    'xtensa':      'CONFIG_XTENSA',\n+}\n+\n+\n+def _canon_bitmask_expr(expr, table):\n+    \"\"\"Canonicalise a bitmask expression (e.g. signal direction/timing,\n+    side-effect flags). Accepts `|`- or `,`-joined tokens and returns a\n+    `|`-joined canonical KAPI_* string.\"\"\"\n+    if not expr:\n+        return expr\n+    sep = ',' if ',' in expr and '|' not in expr else '|'\n+    tokens = [_canon_token(t, table) for t in expr.split(sep)]\n+    return ' | '.join(t for t in tokens if t)\n+\n+\n+# Types that carry user-space pointer semantics. A param with one of\n+# these types implicitly gets KAPI_PARAM_USER.\n+_IMPLIES_USER_FLAG = {'KAPI_TYPE_USER_PTR', 'KAPI_TYPE_PATH'}\n+\n+\n+def _split_type_line(value):\n+    \"\"\"Split a 'type:' line into (type, [flags...]).\n+\n+    Accepts a single-token value (e.g. 'KAPI_TYPE_UINT' or 'uint')\n+    leaving flags empty, or a comma-separated form\n+    (e.g. 'uint, input, user') where the first token is the type and\n+    subsequent tokens are flag aliases.\n+\n+    When the type is user-space (user_ptr, path), KAPI_PARAM_USER is\n+    added to the flags list if not already present.\"\"\"\n+    parts = [p.strip() for p in value.split(',') if p.strip()]\n+    if not parts:\n+        return None, []\n+    ty = _canon_token(parts[0], _TYPE_ALIASES)\n+    flags = [_canon_token(f, _FLAG_ALIASES) for f in parts[1:]]\n+    if ty in _IMPLIES_USER_FLAG and 'KAPI_PARAM_USER' not in flags:\n+        flags.append('KAPI_PARAM_USER')\n+    return ty, flags\n+\n+\n+def _split_constraint_expr(value):\n+    \"\"\"Parse a constraint expression into (canonical_type, extras).\n+\n+    Shapes:\n+        NAME                              e.g. 'user_path', 'nonzero'\n+        NAME ( ARG (, ARG)* )             e.g. 'range(0, 4096)', 'buffer(2)'\n+\n+    Returns None for free text. Otherwise returns\n+    (constraint_type, {aux_field: value, ...}) where the aux fields map\n+    onto the matching param-range / param-mask / param-size /\n+    param-enum-values / param-constraint slots.\n+    \"\"\"\n+    t = value.strip()\n+    if not t:\n+        return None\n+    # Split NAME ( ARGS )\n+    lp = t.find('(')\n+    rp = t.rfind(')')\n+    if lp \u003e 0 and rp \u003e lp:\n+        name = t[:lp].strip()\n+        args_raw = t[lp + 1:rp].strip()\n+    elif lp \u003c 0:\n+        name = t\n+        args_raw = None\n+    else:\n+        return None\n+    # Bareword must be a single identifier; multi-word values are free text.\n+    if not name or any(c.isspace() for c in name):\n+        return None\n+    key = name.lower()\n+    table = {\n+        'range':          ('KAPI_CONSTRAINT_RANGE',       'param-range'),\n+        'mask':           ('KAPI_CONSTRAINT_MASK',        'param-mask'),\n+        'enum':           ('KAPI_CONSTRAINT_ENUM',        'param-enum-values'),\n+        'alignment':      ('KAPI_CONSTRAINT_ALIGNMENT',   'param-alignment'),\n+        'align':          ('KAPI_CONSTRAINT_ALIGNMENT',   'param-alignment'),\n+        'power_of_two':   ('KAPI_CONSTRAINT_POWER_OF_TWO', None),\n+        'page_aligned':   ('KAPI_CONSTRAINT_PAGE_ALIGNED', None),\n+        'nonzero':        ('KAPI_CONSTRAINT_NONZERO',      None),\n+        'user_string':    ('KAPI_CONSTRAINT_USER_STRING',  'param-size'),\n+        'user_path':      ('KAPI_CONSTRAINT_USER_PATH',    None),\n+        'user_ptr':       ('KAPI_CONSTRAINT_USER_PTR',     None),\n+        'buffer':         ('KAPI_CONSTRAINT_BUFFER',       'param-size-param'),\n+        'custom':         ('KAPI_CONSTRAINT_CUSTOM',       'param-constraint'),\n+    }\n+    if key not in table:\n+        return None\n+    ctype, aux_key = table[key]\n+    extras = {}\n+    if aux_key and args_raw is not None:\n+        extras[aux_key] = args_raw\n+    return ctype, extras\n+\n+\n+# Subfield names consumed per block type. An indented line opens a new\n+# subfield only when it starts with one of these followed by ':'; any\n+# other line continues the previous subfield.\n+_SIGNAL_SUBFIELDS = frozenset({\n+    'direction', 'action', 'condition', 'desc', 'errno', 'timing',\n+    'priority', 'restartable', 'interruptible', 'number', 'target',\n+    'queue', 'queue_behavior', 'transform', 'transform_to', 'transform-to',\n+    'sa_flags_required', 'sa-flags-required',\n+    'sa_flags_forbidden', 'sa-flags-forbidden',\n+    'state_required', 'state-required',\n+    'state_forbidden', 'state-forbidden',\n+})\n+_LOCK_SUBFIELDS = frozenset({\n+    'type', 'scope', 'acquired', 'released', 'held-on-entry',\n+    'held-on-exit', 'desc',\n+})\n+_CONSTRAINT_SUBFIELDS = frozenset({'desc', 'expr'})\n+_SIDE_EFFECT_SUBFIELDS = frozenset({'target', 'desc', 'condition', 'reversible'})\n+_STATE_TRANS_SUBFIELDS = frozenset({'object', 'from', 'to', 'condition', 'desc'})\n+_CAPABILITY_SUBFIELDS = frozenset({\n+    'type', 'allows', 'without', 'condition', 'priority', 'desc',\n+})\n+_RETURN_SUBFIELDS = frozenset({\n+    'type', 'check-type', 'success', 'success-range', 'error-values', 'desc',\n+})\n+\n+_RETURN_INT = r'-?(?:0[xX][0-9a-fA-F]+|0[bB][01]+|\\d+)[uUlL]*'\n+_RETURN_EXACT_RE = re.compile(rf'^(?:==?\\s*)?({_RETURN_INT})$')\n+_RETURN_RANGE_RE = re.compile(rf'^\u003e=\\s*({_RETURN_INT})$')\n+\n+\n+def _fold_paragraphs(content):\n+    \"\"\"Fold free-form prose into paragraphs.\n+\n+    Blank lines separate paragraphs (\"\\\\n\\\\n\"); wrapped lines inside a\n+    paragraph are joined with spaces, except that a line starting with\n+    \"- \" always begins a new line so bullet lists survive.\"\"\"\n+    paragraphs = []\n+    current = []\n+    for line in content.split('\\n'):\n+        line = line.strip()\n+        if not line:\n+            if current:\n+                paragraphs.append(current)\n+                current = []\n+        elif line.startswith('- ') or not current:\n+            current.append(line)\n+        else:\n+            current[-1] += ' ' + line\n+    if current:\n+        paragraphs.append(current)\n+    return '\\n\\n'.join('\\n'.join(p) for p in paragraphs) or None\n+\n+\n+def _fold_lines(content):\n+    \"\"\"Keep every line of a block on its own line.\n+\n+    The indentation shared by the continuation lines is removed so that\n+    relative indentation (nested code) is preserved; runs of blank lines\n+    collapse into one.\"\"\"\n+    lines = [line.rstrip() for line in content.expandtabs().split('\\n')]\n+    while lines and not lines[0]:\n+        lines.pop(0)\n+    while lines and not lines[-1]:\n+        lines.pop()\n+    if not lines:\n+        return None\n+\n+    indents = [len(line) - len(line.lstrip()) for line in lines[1:] if line]\n+    base = min(indents) if indents else 0\n+\n+    out = []\n+    for line in lines:\n+        if line:\n+            line = line[min(base, len(line) - len(line.lstrip())):]\n+        elif out and not out[-1]:\n+            continue\n+        out.append(line)\n+    return '\\n'.join(out)\n+\n+\n+def _return_success_macro(check_type, value):\n+    \"\"\"Translate a return `success:` value into the macro matching the\n+    check type, or None when the check type does not use one (or the\n+    value is not a plain integer expression).\"\"\"\n+    if check_type in ('', 'KAPI_RETURN_EXACT'):\n+        m = _RETURN_EXACT_RE.match(value)\n+        return f\"KAPI_RETURN_SUCCESS({m.group(1)})\" if m else None\n+    if check_type == 'KAPI_RETURN_RANGE':\n+        m = _RETURN_RANGE_RE.match(value)\n+        return f\"KAPI_RETURN_SUCCESS_RANGE({m.group(1) if m else 0}, S64_MAX)\"\n+    return None\n+\n+\n+class ApiSpecFormat(OutputFormat):\n+    \"\"\"Generate C macro invocations for kernel API specifications\"\"\"\n+\n+    def __init__(self):\n+        super().__init__()\n+        self.header_written = False\n+\n+    def msg(self, fname, name, args):\n+        \"\"\"Handles a single entry from kernel-doc parser\"\"\"\n+        if not self.header_written:\n+            header = self._generate_header()\n+            self.header_written = True\n+        else:\n+            header = \"\"\n+\n+        self.data = \"\"\n+        result = super().msg(fname, name, args)\n+        return header + (result if result else self.data)\n+\n+    def _generate_header(self):\n+        \"\"\"Generate the file header\"\"\"\n+        return (\n+            \"/* SPDX-License-Identifier: GPL-2.0 */\\n\"\n+            \"/* Auto-generated from kerneldoc annotations - DO NOT EDIT */\\n\\n\"\n+            \"#include \u003clinux/capability.h\u003e\\n\"\n+            \"#include \u003clinux/errno.h\u003e\\n\"\n+            \"#include \u003clinux/fcntl.h\u003e\\n\"\n+            \"#include \u003clinux/kernel_api_spec.h\u003e\\n\"\n+            \"#include \u003clinux/signal.h\u003e\\n\"\n+            \"#include \u003clinux/stat.h\u003e\\n\\n\"\n+        )\n+\n+    def _format_macro_param(self, value, multiline=False):\n+        \"\"\"Format a value for use in C macro parameter.\n+\n+        Every string field in the kernel structs is a `const char *`, so\n+        there is no length limit. Newlines become spaces unless\n+        `multiline` is set, in which case they are kept as \"\\\\n\" escapes.\n+        \"\"\"\n+        if value is None:\n+            return '\"\"'\n+        value = str(value).replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')\n+        value = value.replace('\\t', ' ').replace('\\r', '').replace('\\0', '')\n+        value = value.replace('\\n', '\\\\n' if multiline else ' ')\n+        return f'\"{value}\"'\n+\n+    def _get_section(self, sections, key):\n+        \"\"\"Get first line from sections, checking with and without @ prefix and case variants\"\"\"\n+        for variant in [key, key.capitalize(), key.title()]:\n+            for prefix in ['', '@']:\n+                full_key = prefix + variant\n+                if full_key in sections:\n+                    content = sections[full_key].strip()\n+                    # Return only first line to avoid mixing sections\n+                    return content.split('\\n')[0].strip() if content else ''\n+        return None\n+\n+    def _get_raw_section(self, sections, key):\n+        \"\"\"Get full section content, checking with and without @ prefix and case variants\"\"\"\n+        for variant in [key, key.capitalize(), key.title()]:\n+            for prefix in ['', '@']:\n+                full_key = prefix + variant\n+                if full_key in sections:\n+                    return sections[full_key]\n+        return ''\n+\n+    def _get_multiline_section(self, sections, key, fold=_fold_paragraphs):\n+        \"\"\"Get a multi-line section, structured by `fold`.\n+\n+        This is used for fields like notes, long-desc, and examples that\n+        can span multiple lines in the kerneldoc comment.\n+        \"\"\"\n+        content = self._get_raw_section(sections, key)\n+        return fold(content) if content else None\n+\n+    def _parse_indented_items(self, section_content, item_parser):\n+        \"\"\"Generic parser for indented items.\n+\n+        Args:\n+            section_content: Raw section content\n+            item_parser: Function that takes (lines, start_index) and returns (item, next_index)\n+\n+        Returns:\n+            List of parsed items\n+        \"\"\"\n+        if not section_content:\n+            return []\n+\n+        items = []\n+        lines = section_content.strip().split('\\n')\n+        i = 0\n+\n+        while i \u003c len(lines):\n+            if not lines[i].strip():\n+                i += 1\n+                continue\n+\n+            # Check if this is a main item (not indented)\n+            if not lines[i].startswith((' ', '\\t')):\n+                item, i = item_parser(lines, i)\n+                if item:\n+                    items.append(item)\n+            else:\n+                i += 1\n+\n+        return items\n+\n+    def _parse_subfields(self, lines, start_idx, keys):\n+        \"\"\"Parse indented subfields starting from start_idx+1.\n+\n+        A line opens a new subfield only if it starts with one of `keys`\n+        followed by ':'; every other line continues the previous one.\n+        Blank lines inside the block are skipped when more indented\n+        lines follow.\n+\n+        Returns: (dict of subfields, next index)\n+        \"\"\"\n+        subfields = {}\n+        i = start_idx + 1\n+\n+        current_key = None\n+        while i \u003c len(lines):\n+            if not lines[i].strip():\n+                nxt = i + 1\n+                while nxt \u003c len(lines) and not lines[nxt].strip():\n+                    nxt += 1\n+                if nxt == len(lines) or not lines[nxt].startswith((' ', '\\t')):\n+                    break\n+                i = nxt\n+                continue\n+            if not lines[i].startswith((' ', '\\t')):\n+                break\n+            line = lines[i].strip()\n+            key, sep, value = line.partition(':')\n+            key = key.strip()\n+            if sep and key in keys:\n+                current_key = key\n+                subfields[current_key] = value.strip()\n+            elif current_key:\n+                subfields[current_key] = (subfields[current_key] + ' ' + line).strip()\n+            i += 1\n+\n+        return subfields, i\n+\n+    def _parse_signal_item(self, lines, i):\n+        \"\"\"Parse a single signal specification\"\"\"\n+        signal = {'name': lines[i].strip()}\n+        subfields, next_i = self._parse_subfields(lines, i, _SIGNAL_SUBFIELDS)\n+\n+        # `direction` and `timing` are bitmasks of KAPI_SIGNAL_* /\n+        # KAPI_SIGNAL_TIME_* values; `action` is a single\n+        # KAPI_SIGNAL_ACTION_* enum. All three canonicalise aliases to\n+        # their KAPI_* spelling.\n+        raw_direction = subfields.get('direction', 'KAPI_SIGNAL_RECEIVE')\n+        raw_action    = subfields.get('action',    'KAPI_SIGNAL_ACTION_RETURN')\n+        raw_timing    = subfields.get('timing')\n+        # `errno:` carries the signal's errno-on-return. The plain\n+        # `error:` spelling cannot be used inside a signal block\n+        # because kerneldoc promotes it to a top-level `error:` section.\n+        signal.update({\n+            'direction':     _canon_bitmask_expr(raw_direction, _SIGNAL_DIR_ALIASES),\n+            'action':        _canon_token(raw_action, _SIGNAL_ACTION_ALIASES),\n+            'condition':     subfields.get('condition'),\n+            'desc':          subfields.get('desc'),\n+            'error':         subfields.get('errno'),\n+            'timing':        _canon_bitmask_expr(raw_timing, _SIGNAL_TIMING_ALIASES)\n+                              if raw_timing else None,\n+            'priority':      subfields.get('priority'),\n+            'restartable':   subfields.get('restartable', '').lower() == 'yes',\n+            'interruptible': subfields.get('interruptible', '').lower() == 'yes',\n+            'number':        subfields.get('number', '0'),\n+            # Additional struct fields. These are optional; if absent, no\n+            # KAPI_SIGNAL_* macro is emitted and the field stays at its\n+            # zero-initialised default.\n+            'target':        subfields.get('target'),\n+            'queue':         subfields.get('queue') or subfields.get('queue_behavior'),\n+            'transform':     subfields.get('transform') or subfields.get('transform_to')\n+                              or subfields.get('transform-to'),\n+            'sa_flags_required':  subfields.get('sa_flags_required')\n+                                    or subfields.get('sa-flags-required'),\n+            'sa_flags_forbidden': subfields.get('sa_flags_forbidden')\n+                                    or subfields.get('sa-flags-forbidden'),\n+            'state_required':     subfields.get('state_required')\n+                                    or subfields.get('state-required'),\n+            'state_forbidden':    subfields.get('state_forbidden')\n+                                    or subfields.get('state-forbidden'),\n+        })\n+\n+        return signal, next_i\n+\n+    def _parse_error_item(self, lines, i):\n+        \"\"\"Parse a single error specification\"\"\"\n+        line = lines[i].strip()\n+\n+        # Skip desc: lines\n+        if line.startswith('desc:'):\n+            return None, i + 1\n+\n+        # Check for error pattern\n+        if not re.match(r'^[A-Z][A-Z0-9_]+,', line):\n+            return None, i + 1\n+\n+        error = {'line': line, 'desc': ''}\n+\n+        # Look for desc: and condition: subfields\n+        i += 1\n+        desc_lines = []\n+        while i \u003c len(lines):\n+            next_line = lines[i].strip()\n+            if next_line.startswith('desc:'):\n+                desc_lines.append(next_line[5:].strip())\n+                i += 1\n+            elif next_line.startswith('condition:'):\n+                error['condition'] = next_line[10:].strip()\n+                i += 1\n+            elif not next_line:\n+                break\n+            elif not desc_lines and re.match(r'^[A-Z][A-Z0-9_]+,', next_line):\n+                # New error entry, but only if we haven't started a desc block\n+                break\n+            else:\n+                desc_lines.append(next_line)\n+                i += 1\n+\n+        if desc_lines:\n+            error['desc'] = ' '.join(desc_lines)\n+\n+        return error, i\n+\n+    def _parse_lock_item(self, lines, i):\n+        \"\"\"Parse a single lock specification.\n+\n+        Two shapes are accepted:\n+          * inline `NAME, TYPE` on the main line; or\n+          * `NAME` on the main line with `type:` as an indented\n+            subfield.\n+        Lock-type values are canonicalised to KAPI_LOCK_* spellings.\n+        \"\"\"\n+        head = lines[i].strip()\n+        if not head:\n+            return None, i + 1\n+\n+        parts = head.split(',', 1)\n+        subfields, next_i = self._parse_subfields(lines, i, _LOCK_SUBFIELDS)\n+\n+        name = parts[0].strip()\n+        type_raw = (parts[1].strip() if len(parts) \u003e= 2\n+                    else subfields.get('type', '').strip())\n+        if not name or not type_raw:\n+            return None, next_i\n+\n+        lock = {\n+            'name': name,\n+            'type': _canon_token(type_raw, _LOCK_TYPE_ALIASES),\n+        }\n+\n+        for field in ['acquired', 'released', 'held-on-entry', 'held-on-exit']:\n+            if subfields.get(field, '').lower() in ('true', 'yes'):\n+                lock[field] = True\n+\n+        lock['desc'] = subfields.get('desc', '')\n+\n+        return lock, next_i\n+\n+    def _parse_constraint_item(self, lines, i):\n+        \"\"\"Parse a single constraint specification\"\"\"\n+        line = lines[i].strip()\n+\n+        # NAME, description form\n+        if ',' in line:\n+            parts = line.split(',', 1)\n+            constraint = {\n+                'name': parts[0].strip(),\n+                'desc': parts[1].strip() if len(parts) \u003e 1 else '',\n+                'expr': None\n+            }\n+        else:\n+            constraint = {'name': line, 'desc': '', 'expr': None}\n+\n+        subfields, next_i = self._parse_subfields(lines, i, _CONSTRAINT_SUBFIELDS)\n+\n+        if 'desc' in subfields:\n+            constraint['desc'] = (constraint['desc'] + ' ' + subfields['desc']).strip()\n+        constraint['expr'] = subfields.get('expr')\n+\n+        return constraint, next_i\n+\n+    def _parse_side_effect_item(self, lines, i):\n+        \"\"\"Parse a single side effect specification\"\"\"\n+        line = lines[i].strip()\n+\n+        # Defaults for the subfield form\n+        effect = {\n+            'type': line,\n+            'target': '',\n+            'desc': '',\n+            'condition': None,\n+            'reversible': False\n+        }\n+\n+        # Inline comma-separated form\n+        if ',' in line:\n+            # Handle condition and reversible flags\n+            cond_match = re.search(r',\\s*condition=([^,]+?)(?:\\s*,\\s*reversible=(yes|no)\\s*)?$', line)\n+            if cond_match:\n+                effect['condition'] = cond_match.group(1).strip()\n+                effect['reversible'] = cond_match.group(2) == 'yes'\n+                line = line[:cond_match.start()]\n+            elif ', reversible=yes' in line:\n+                effect['reversible'] = True\n+                line = line.replace(', reversible=yes', '')\n+            elif ', reversible=no' in line:\n+                line = line.replace(', reversible=no', '')\n+\n+            parts = line.split(',', 2)\n+            if len(parts) \u003e= 1:\n+                effect['type'] = parts[0].strip()\n+            if len(parts) \u003e= 2:\n+                effect['target'] = parts[1].strip()\n+            if len(parts) \u003e= 3:\n+                effect['desc'] = parts[2].strip()\n+        else:\n+            # Multi-line format with subfields\n+            subfields, next_i = self._parse_subfields(\n+                lines, i, _SIDE_EFFECT_SUBFIELDS)\n+            effect.update({\n+                'target': subfields.get('target', ''),\n+                'desc': subfields.get('desc', ''),\n+                'condition': subfields.get('condition'),\n+                'reversible': subfields.get('reversible', '').lower() == 'yes'\n+            })\n+            return effect, next_i\n+\n+        return effect, i + 1\n+\n+    def _parse_state_trans_item(self, lines, i):\n+        \"\"\"Parse a single state transition specification\"\"\"\n+        line = lines[i].strip()\n+\n+        trans = {\n+            'target': line,\n+            'from': '',\n+            'to': '',\n+            'condition': '',\n+            'desc': ''\n+        }\n+\n+        # Inline comma-separated form\n+        if ',' in line:\n+            parts = line.split(',', 3)\n+            if len(parts) \u003e= 1:\n+                trans['target'] = parts[0].strip()\n+            if len(parts) \u003e= 2:\n+                trans['from'] = parts[1].strip()\n+            if len(parts) \u003e= 3:\n+                trans['to'] = parts[2].strip()\n+            if len(parts) \u003e= 4:\n+                trans['desc'] = parts[3].strip()\n+            return trans, i + 1\n+        else:\n+            # Multi-line format with subfields\n+            subfields, next_i = self._parse_subfields(\n+                lines, i, _STATE_TRANS_SUBFIELDS)\n+            trans.update({\n+                'target': subfields.get('object', line),\n+                'from': subfields.get('from', ''),\n+                'to': subfields.get('to', ''),\n+                'condition': subfields.get('condition', ''),\n+                'desc': subfields.get('desc', '')\n+            })\n+            return trans, next_i\n+\n+    def _process_parameters(self, sections, parameterlist, parameterdescs, parametertypes):\n+        \"\"\"Process and output parameter specifications\"\"\"\n+        param_count = len(parameterlist)\n+        if param_count \u003e 0:\n+            self.data += f\"\\n\\tKAPI_PARAM_COUNT({param_count})\\n\"\n+\n+        for param_idx, param in enumerate(parameterlist):\n+            param_name = param.strip()\n+            param_desc = parameterdescs.get(param_name, '').strip()\n+            param_ctype = parametertypes.get(param_name, '')\n+\n+            # Parse parameter specifications\n+            param_section = self._get_raw_section(sections, 'param')\n+            param_specs = {}\n+            if param_section:\n+                param_specs = self._parse_param_spec(param_section, param_name)\n+\n+            self.data += f\"\\n\\tKAPI_PARAM({param_idx}, {self._format_macro_param(param_name)}, \"\n+            self.data += f\"{self._format_macro_param(param_ctype)}, {self._format_macro_param(param_desc)})\\n\"\n+\n+            # Add parameter attributes\n+            for key, macro in [\n+                ('param-type', 'KAPI_PARAM_TYPE'),\n+                ('param-flags', 'KAPI_PARAM_FLAGS'),\n+                ('param-size', 'KAPI_PARAM_SIZE'),\n+                ('param-alignment', 'KAPI_PARAM_ALIGNMENT'),\n+            ]:\n+                if key in param_specs:\n+                    self.data += f\"\\t\\t{macro}({param_specs[key]})\\n\"\n+\n+            # Handle constraint type\n+            if 'param-constraint-type' in param_specs:\n+                ctype = param_specs['param-constraint-type']\n+                self.data += f\"\\t\\tKAPI_PARAM_CONSTRAINT_TYPE({ctype})\\n\"\n+\n+            # Handle range\n+            if 'param-range' in param_specs and ',' in param_specs['param-range']:\n+                min_val, max_val = param_specs['param-range'].split(',', 1)\n+                self.data += f\"\\t\\tKAPI_PARAM_RANGE({min_val.strip()}, {max_val.strip()})\\n\"\n+\n+            # Handle mask. If `arch-mask:` lines are present, fall back\n+            # to a raw `.valid_mask = (...)` initializer so we can wrap\n+            # arch-specific bits in #ifdef CONFIG_\u003cARCH\u003e ... #endif, which\n+            # is illegal inside a function-like macro argument.\n+            if 'param-mask' in param_specs:\n+                arch_masks = param_specs.get('param-arch-mask') or []\n+                if arch_masks:\n+                    base = param_specs['param-mask']\n+                    self.data += f\"\\t\\t.valid_mask = ({base})\"\n+                    for arch, bits in arch_masks:\n+                        config = _ARCH_CONFIG.get(arch.lower())\n+                        if config is None:\n+                            sys.stderr.write(\n+                                f\"kdoc_apispec: unknown arch '{arch}' in \"\n+                                f\"arch-mask: line; skipping\\n\")\n+                            continue\n+                        self.data += (f\"\\n#ifdef {config}\\n\"\n+                                      f\"\\t\\t\\t| ({bits})\\n\"\n+                                      f\"#endif\\n\")\n+                    self.data += \"\\t\\t,\\n\"\n+                else:\n+                    self.data += (\n+                        f\"\\t\\tKAPI_PARAM_VALID_MASK(\"\n+                        f\"{param_specs['param-mask']})\\n\")\n+\n+            # Handle enum values\n+            if 'param-enum-values' in param_specs:\n+                self.data += f\"\\t\\tKAPI_PARAM_ENUM_VALUES({param_specs['param-enum-values']})\\n\"\n+\n+            # Handle size parameter index\n+            if 'param-size-param' in param_specs:\n+                self.data += f\"\\t\\tKAPI_PARAM_SIZE_PARAM({param_specs['param-size-param']})\\n\"\n+\n+            # Handle constraint description\n+            if 'param-constraint' in param_specs:\n+                self.data += f\"\\t\\tKAPI_PARAM_CONSTRAINT({self._format_macro_param(param_specs['param-constraint'])})\\n\"\n+\n+            self.data += \"\\t},\\n\"\n+\n+    def _parse_param_spec(self, section_content, param_name):\n+        \"\"\"Parse parameter specifications from indented format\"\"\"\n+        specs = {}\n+        lines = section_content.strip().split('\\n')\n+        current_item = None\n+\n+        # Map to expected keys\n+        field_map = {\n+            'type': 'param-type',\n+            'flags': 'param-flags',\n+            'size': 'param-size',\n+            'constraint-type': 'param-constraint-type',\n+            'constraint': 'param-constraint',\n+            'cdesc': 'param-constraint',\n+            'range': 'param-range',\n+            'mask': 'param-mask',\n+            'valid-mask': 'param-mask',\n+            'valid-values': 'param-enum-values',\n+            'alignment': 'param-alignment',\n+            'size-param': 'param-size-param',\n+            'struct-type': 'param-struct-type',\n+            'arch-mask': 'param-arch-mask',\n+        }\n+\n+        i = 0\n+        while i \u003c len(lines):\n+            line = lines[i]\n+            if not line.strip():\n+                i += 1\n+                continue\n+\n+            # Check if this is our parameter (non-indented line)\n+            if not line.startswith((' ', '\\t')):\n+                parts = line.strip().split(',', 1)\n+                current_item = param_name if parts[0].strip() == param_name else None\n+                if current_item and len(parts) \u003e 1:\n+                    specs['param-type'] = parts[1].strip()\n+                i += 1\n+            elif current_item == param_name:\n+                # Parse subfield\n+                stripped = line.strip()\n+                if ':' in stripped:\n+                    key, value = stripped.split(':', 1)\n+                    key = key.strip()\n+                    value = value.strip()\n+\n+                    # Collect continuation lines (indented lines without a colon that\n+                    # defines a new key, i.e., lines that are pure continuations)\n+                    i += 1\n+                    while i \u003c len(lines):\n+                        next_line = lines[i]\n+                        # Stop if we hit a non-indented line (new param)\n+                        if next_line.strip() and not next_line.startswith((' ', '\\t')):\n+                            break\n+                        next_stripped = next_line.strip()\n+                        # Stop if we hit a new key (contains colon with known key prefix)\n+                        if next_stripped and ':' in next_stripped:\n+                            potential_key = next_stripped.split(':', 1)[0].strip()\n+                            if potential_key in field_map or potential_key in ['type', 'desc']:\n+                                break\n+                        # This is a continuation line\n+                        if next_stripped:\n+                            value = value + ' ' + next_stripped\n+                        i += 1\n+\n+                    if key in field_map:\n+                        # Clean up the value - remove excessive whitespace\n+                        value = ' '.join(value.split())\n+                        mapped = field_map[key]\n+                        if mapped == 'param-type':\n+                            # Single token sets the type; additional\n+                            # comma-separated tokens are flags OR'd\n+                            # into param-flags.\n+                            ty, extra_flags = _split_type_line(value)\n+                            if ty:\n+                                specs['param-type'] = ty\n+                            if extra_flags:\n+                                existing = specs.get('param-flags', '')\n+                                merged = (existing + ' | ' if existing else '') \\\n+                                         + ' | '.join(extra_flags)\n+                                specs['param-flags'] = merged\n+                        elif mapped == 'param-flags':\n+                            specs['param-flags'] = _canon_flags_expr(value)\n+                        elif mapped == 'param-constraint-type':\n+                            # Accepts a KAPI_CONSTRAINT_* token or a\n+                            # function-call expression like\n+                            # `range(0, 4096)` / `mask(0xff)` /\n+                            # `buffer(2)` that also populates the\n+                            # matching aux field.\n+                            parsed = _split_constraint_expr(value)\n+                            if parsed is not None:\n+                                ctype, extras = parsed\n+                                specs['param-constraint-type'] = ctype\n+                                for aux_k, aux_v in extras.items():\n+                                    specs[aux_k] = aux_v\n+                            else:\n+                                specs['param-constraint-type'] = value\n+                        elif mapped == 'param-arch-mask':\n+                            # `arch-mask: \u003carch\u003e = \u003cbits\u003e`: multiple\n+                            # entries accumulate into a list of\n+                            # (arch, bits) tuples that the emitter\n+                            # turns into per-arch #ifdef-guarded mask\n+                            # contributions.\n+                            if '=' in value:\n+                                arch, bits = value.split('=', 1)\n+                                specs.setdefault(mapped, []).append(\n+                                    (arch.strip(), bits.strip()))\n+                        else:\n+                            specs[mapped] = value\n+                else:\n+                    i += 1\n+            else:\n+                i += 1\n+\n+        return specs\n+\n+    def _validate_effect_type(self, effect_type):\n+        \"\"\"Validate and normalize effect type\"\"\"\n+        if 'KAPI_EFFECT_' in effect_type and effect_type not in VALID_EFFECT_TYPES:\n+            if '|' in effect_type:\n+                parts = [p.strip() for p in effect_type.split('|')]\n+                valid_parts = []\n+                for p in parts:\n+                    if p in VALID_EFFECT_TYPES:\n+                        valid_parts.append(p)\n+                    else:\n+                        print(f\"warning: unrecognized effect type '{p}', \"\n+                              f\"defaulting to KAPI_EFFECT_MODIFY_STATE\", file=sys.stderr)\n+                        valid_parts.append('KAPI_EFFECT_MODIFY_STATE')\n+                return ' | '.join(valid_parts)\n+            print(f\"warning: unrecognized effect type '{effect_type}', \"\n+                  f\"defaulting to KAPI_EFFECT_MODIFY_STATE\", file=sys.stderr)\n+            return 'KAPI_EFFECT_MODIFY_STATE'\n+\n+        return effect_type\n+\n+    def _has_api_spec(self, sections):\n+        \"\"\"Check if this function has an API specification.\n+\n+        Returns True if a `contexts:` or `context-flags:` section is present\n+        together with at least one other KAPI section. Regular kernel-doc\n+        comments that only use a common section name like 'return' or\n+        'error' do not produce a spec.\n+        \"\"\"\n+        context_keys = ['context-flags', 'contexts']\n+        indicators = [\n+            'api-type', 'param', 'error', 'capability', 'signal', 'lock',\n+            'state-trans', 'constraint', 'side-effect', 'long-desc'\n+        ]\n+\n+        def has_section(names):\n+            return any(key.lower().startswith(name) or\n+                       key.lower().startswith('@' + name)\n+                       for key in sections.keys() for name in names)\n+\n+        return has_section(context_keys) and has_section(indicators)\n+\n+    def out_function(self, fname, name, args):\n+        \"\"\"Generate API spec for a function\"\"\"\n+        function_name = args.get('function', name)\n+        sections = args.sections if hasattr(args, 'sections') else args.get('sections', {})\n+\n+        if not self._has_api_spec(sections):\n+            return\n+\n+        parameterlist = args.parameterlist if hasattr(args, 'parameterlist') else args.get('parameterlist', [])\n+        parameterdescs = args.parameterdescs if hasattr(args, 'parameterdescs') else args.get('parameterdescs', {})\n+        parametertypes = args.parametertypes if hasattr(args, 'parametertypes') else args.get('parametertypes', {})\n+        purpose = args.get('purpose', '')\n+\n+        # Start macro invocation\n+        self.data += f\"DEFINE_KERNEL_API_SPEC({function_name})\\n\"\n+\n+        # Basic info\n+        if purpose:\n+            self.data += f\"\\tKAPI_DESCRIPTION({self._format_macro_param(purpose)})\\n\"\n+\n+        long_desc = self._get_multiline_section(sections, 'long-desc')\n+        if long_desc:\n+            self.data += f\"\\tKAPI_LONG_DESC({self._format_macro_param(long_desc, True)})\\n\"\n+\n+        # Context flags. `contexts:` and `context-flags:` both work;\n+        # tokens canonicalise to KAPI_CTX_* for KAPI_CONTEXT().\n+        context = (self._get_section(sections, 'contexts')\n+                   or self._get_section(sections, 'context-flags'))\n+        if context:\n+            self.data += f\"\\tKAPI_CONTEXT({_canon_context_expr(context)})\\n\"\n+\n+        # Process parameters\n+        self._process_parameters(sections, parameterlist, parameterdescs, parametertypes)\n+\n+        # Process return value\n+        self._process_return(sections)\n+\n+        # Process errors\n+        errors = self._parse_indented_items(\n+            self._get_raw_section(sections, 'error'),\n+            self._parse_error_item\n+        )\n+\n+        if errors:\n+            self.data += f\"\\n\\tKAPI_ERROR_COUNT({len(errors)})\\n\"\n+\n+            for idx, error in enumerate(errors):\n+                self._output_error(idx, error)\n+\n+        # Process signals\n+        signals = self._parse_indented_items(\n+            self._get_raw_section(sections, 'signal'),\n+            self._parse_signal_item\n+        )\n+\n+        if signals:\n+            self.data += f\"\\n\\tKAPI_SIGNAL_COUNT({len(signals)})\\n\"\n+\n+            for idx, signal in enumerate(signals):\n+                self._output_signal(idx, signal)\n+\n+        # Process other specifications\n+        self._process_locks(sections)\n+        self._process_constraints(sections)\n+        self._process_side_effects(sections)\n+        self._process_state_transitions(sections)\n+        self._process_capabilities(sections)\n+\n+        # Add examples and notes\n+        for key, macro, fold in [\n+            ('examples', 'KAPI_EXAMPLES', _fold_lines),\n+            ('notes', 'KAPI_NOTES', _fold_paragraphs),\n+        ]:\n+            value = self._get_multiline_section(sections, key, fold)\n+            if value:\n+                self.data += f\"\\n\\t{macro}({self._format_macro_param(value, True)})\\n\"\n+\n+        self.data += \"\\n};\\n\\n\"\n+\n+    def _process_return(self, sections):\n+        \"\"\"Process the return value specification from kerneldoc annotations\"\"\"\n+        raw = self._get_raw_section(sections, 'return')\n+        if not raw:\n+            return\n+\n+        # Parse subfields from the return section, handling continuation lines\n+        lines = raw.strip().split('\\n')\n+        subfields = {}\n+        current_key = None\n+        for line in lines:\n+            stripped = line.strip()\n+            key, sep, value = stripped.partition(':')\n+            key = key.strip()\n+            if sep and key in _RETURN_SUBFIELDS:\n+                current_key = key\n+                subfields[current_key] = value.strip()\n+            elif current_key and stripped:\n+                # Continuation line\n+                subfields[current_key] += ' ' + stripped\n+\n+        ret_type = subfields.get('type', '')\n+        check_type = subfields.get('check-type', '')\n+        desc = subfields.get('desc', '')\n+        success = subfields.get('success', '')\n+\n+        if not ret_type and not desc:\n+            return\n+\n+        # Canonicalise short aliases:\n+        #   type: int             -\u003e KAPI_TYPE_INT\n+        #   check-type: fd        -\u003e KAPI_RETURN_FD\n+        # The type name itself is kept as written.\n+        type_name = ret_type\n+        if ret_type:\n+            ret_type = _canon_token(ret_type, _TYPE_ALIASES)\n+        if check_type:\n+            check_type = _canon_token(check_type, _RETURN_CHECK_ALIASES)\n+\n+        self.data += f\"\\n\\tKAPI_RETURN({self._format_macro_param(type_name)}, \"\n+        self.data += f\"{self._format_macro_param(desc)})\\n\"\n+\n+        if ret_type:\n+            self.data += f\"\\t\\tKAPI_RETURN_TYPE({ret_type})\\n\"\n+\n+        if check_type:\n+            self.data += f\"\\t\\tKAPI_RETURN_CHECK_TYPE({check_type})\\n\"\n+\n+        if success:\n+            macro = _return_success_macro(check_type, success)\n+            if macro:\n+                self.data += f\"\\t\\t{macro}\\n\"\n+            elif check_type in ('', 'KAPI_RETURN_EXACT'):\n+                sys.stderr.write(\n+                    f\"kdoc_apispec: ignoring unsupported success value \"\n+                    f\"'{success}' for an exact return check\\n\")\n+\n+        self.data += \"\\t},\\n\"\n+\n+    def _output_error(self, idx, error):\n+        \"\"\"Output a single error specification\"\"\"\n+        # Format: NAME, description\n+        parts = error['line'].split(',', 1)\n+        if len(parts) \u003c 2:\n+            return\n+\n+        name = parts[0].strip()\n+        short_desc = parts[1].strip()\n+        code = f\"-{name}\"\n+\n+        condition = error.get('condition') or short_desc\n+        long_desc = error.get('desc', '') or short_desc\n+\n+        self.data += f\"\\n\\tKAPI_ERROR({idx}, {code}, {self._format_macro_param(name)}, \"\n+        self.data += f\"{self._format_macro_param(condition)},\\n\\t\\t   {self._format_macro_param(long_desc)})\\n\"\n+\n+    def _output_signal(self, idx, signal):\n+        \"\"\"Output a single signal specification\"\"\"\n+        self.data += f\"\\n\\tKAPI_SIGNAL({idx}, {signal['number']}, \"\n+        self.data += f\"{self._format_macro_param(signal['name'])}, \"\n+        self.data += f\"{signal['direction']}, {signal['action']})\\n\"\n+\n+        # String-valued subfields emitted as KAPI_SIGNAL_* macros.\n+        if signal.get('condition'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_CONDITION({self._format_macro_param(signal['condition'])})\\n\"\n+        if signal.get('desc'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_DESC({self._format_macro_param(signal['desc'])})\\n\"\n+        if signal.get('error'):\n+            # KAPI_SIGNAL_ERROR expects a numeric/token expression\n+            # (e.g. -EINTR), not a quoted string.\n+            self.data += f\"\\t\\tKAPI_SIGNAL_ERROR({signal['error']})\\n\"\n+\n+        # Enum-valued subfields emitted as unquoted tokens.\n+        if signal.get('timing'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_TIMING({signal['timing']})\\n\"\n+        if signal.get('priority'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_PRIORITY({signal['priority']})\\n\"\n+\n+        # Boolean flag subfields.\n+        if signal.get('restartable'):\n+            self.data += \"\\t\\tKAPI_SIGNAL_RESTARTABLE\\n\"\n+        if signal.get('interruptible'):\n+            self.data += \"\\t\\tKAPI_SIGNAL_INTERRUPTIBLE\\n\"\n+\n+        # Additional struct fields, emitted only when present in the\n+        # kerneldoc.\n+        if signal.get('target'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_TARGET({self._format_macro_param(signal['target'])})\\n\"\n+        if signal.get('queue'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_QUEUE({self._format_macro_param(signal['queue'])})\\n\"\n+        if signal.get('transform'):\n+            # Numeric/token expression (e.g. SIGKILL), not a quoted string.\n+            self.data += f\"\\t\\tKAPI_SIGNAL_TRANSFORM({signal['transform']})\\n\"\n+        if signal.get('sa_flags_required'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_SA_FLAGS_REQ({signal['sa_flags_required']})\\n\"\n+        if signal.get('sa_flags_forbidden'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_SA_FLAGS_FORBID({signal['sa_flags_forbidden']})\\n\"\n+        if signal.get('state_required'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_STATE_REQ({signal['state_required']})\\n\"\n+        if signal.get('state_forbidden'):\n+            self.data += f\"\\t\\tKAPI_SIGNAL_STATE_FORBID({signal['state_forbidden']})\\n\"\n+\n+        self.data += \"\\t},\\n\"\n+\n+    def _process_locks(self, sections):\n+        \"\"\"Process lock specifications\"\"\"\n+        locks = self._parse_indented_items(\n+            self._get_raw_section(sections, 'lock'),\n+            self._parse_lock_item\n+        )\n+\n+        if locks:\n+            self.data += f\"\\n\\tKAPI_LOCK_COUNT({len(locks)})\\n\"\n+\n+            for idx, lock in enumerate(locks):\n+                self.data += f\"\\n\\tKAPI_LOCK({idx}, {self._format_macro_param(lock['name'])}, {lock['type']})\\n\"\n+\n+                # `.scope` is zero-initialised to KAPI_LOCK_INTERNAL\n+                # (acquired-and-released). Emit KAPI_LOCK_ACQUIRED /\n+                # KAPI_LOCK_RELEASED only when exactly one of the flags\n+                # is true; emitting both would double-initialise `.scope`\n+                # which breaks `-Werror=override-init` at W=1.\n+                acquired = bool(lock.get('acquired'))\n+                released = bool(lock.get('released'))\n+                if acquired and not released:\n+                    self.data += \"\\t\\tKAPI_LOCK_ACQUIRED\\n\"\n+                elif released and not acquired:\n+                    self.data += \"\\t\\tKAPI_LOCK_RELEASED\\n\"\n+\n+                if lock.get('desc'):\n+                    self.data += f\"\\t\\tKAPI_LOCK_DESC({self._format_macro_param(lock['desc'])})\\n\"\n+\n+                self.data += \"\\t},\\n\"\n+\n+    def _process_constraints(self, sections):\n+        \"\"\"Process constraint specifications\"\"\"\n+        constraints = self._parse_indented_items(\n+            self._get_raw_section(sections, 'constraint'),\n+            self._parse_constraint_item\n+        )\n+\n+        if constraints:\n+            self.data += f\"\\n\\tKAPI_CONSTRAINT_COUNT({len(constraints)})\\n\"\n+\n+            for idx, constraint in enumerate(constraints):\n+                self.data += f\"\\n\\tKAPI_CONSTRAINT({idx}, {self._format_macro_param(constraint['name'])},\\n\"\n+                self.data += f\"\\t\\t\\t{self._format_macro_param(constraint['desc'])})\\n\"\n+\n+                if constraint.get('expr'):\n+                    self.data += f\"\\t\\tKAPI_CONSTRAINT_EXPR({self._format_macro_param(constraint['expr'])})\\n\"\n+\n+                self.data += \"\\t},\\n\"\n+\n+    def _process_side_effects(self, sections):\n+        \"\"\"Process side effect specifications\"\"\"\n+        effects = self._parse_indented_items(\n+            self._get_raw_section(sections, 'side-effect'),\n+            self._parse_side_effect_item\n+        )\n+\n+        if effects:\n+            self.data += f\"\\n\\tKAPI_SIDE_EFFECT_COUNT({len(effects)})\\n\"\n+\n+            for idx, effect in enumerate(effects):\n+                # Canonicalise aliases (alloc_memory, modify_state, ...)\n+                # to KAPI_EFFECT_*. Accepts '|' or ',' as separators.\n+                effect_type = _canon_bitmask_expr(effect['type'], _EFFECT_ALIASES)\n+                effect_type = self._validate_effect_type(effect_type)\n+\n+                self.data += f\"\\n\\tKAPI_SIDE_EFFECT({idx}, {effect_type},\\n\"\n+                self.data += f\"\\t\\t\\t {self._format_macro_param(effect['target'])},\\n\"\n+                self.data += f\"\\t\\t\\t {self._format_macro_param(effect['desc'])})\\n\"\n+\n+                if effect.get('condition'):\n+                    self.data += f\"\\t\\tKAPI_EFFECT_CONDITION({self._format_macro_param(effect['condition'])})\\n\"\n+\n+                if effect.get('reversible'):\n+                    self.data += \"\\t\\tKAPI_EFFECT_REVERSIBLE\\n\"\n+\n+                self.data += \"\\t},\\n\"\n+\n+    def _process_state_transitions(self, sections):\n+        \"\"\"Process state transition specifications\"\"\"\n+        transitions = self._parse_indented_items(\n+            self._get_raw_section(sections, 'state-trans'),\n+            self._parse_state_trans_item\n+        )\n+\n+        if transitions:\n+            self.data += f\"\\n\\tKAPI_STATE_TRANS_COUNT({len(transitions)})\\n\"\n+\n+            for idx, trans in enumerate(transitions):\n+                self.data += f\"\\n\\tKAPI_STATE_TRANS({idx}, {self._format_macro_param(trans['target'])}, \"\n+                self.data += f\"{self._format_macro_param(trans['from'])}, {self._format_macro_param(trans['to'])},\\n\"\n+                self.data += f\"\\t\\t\\t {self._format_macro_param(trans['desc'])})\\n\"\n+\n+                if trans.get('condition'):\n+                    self.data += f\"\\t\\tKAPI_STATE_TRANS_COND({self._format_macro_param(trans['condition'])})\\n\"\n+\n+                self.data += \"\\t},\\n\"\n+\n+    def _process_capabilities(self, sections):\n+        \"\"\"Process capability specifications\"\"\"\n+        cap_section = self._get_raw_section(sections, 'capability')\n+        if not cap_section:\n+            return\n+\n+        lines = cap_section.strip().split('\\n')\n+        capabilities = []\n+        i = 0\n+\n+        while i \u003c len(lines):\n+            line = lines[i].strip()\n+            # Skip empty lines and subfield lines (they'll be parsed with their parent)\n+            if not line or line.startswith(('allows:', 'without:', 'condition:', 'priority:', 'type:', 'desc:')):\n+                i += 1\n+                continue\n+\n+            cap_info = {'line': line}\n+\n+            # Parse subfields\n+            subfields, next_i = self._parse_subfields(\n+                lines, i, _CAPABILITY_SUBFIELDS)\n+            cap_info.update(subfields)\n+            capabilities.append(cap_info)\n+            i = next_i\n+\n+        if capabilities:\n+            # Filter out \"none\" capabilities (no capability required)\n+            valid_caps = [cap for cap in capabilities if cap['line'].strip().lower() != 'none']\n+\n+            if not valid_caps:\n+                return\n+\n+            self.data += f\"\\n\\tKAPI_CAPABILITY_COUNT({len(valid_caps)})\\n\"\n+\n+            for idx, cap in enumerate(valid_caps):\n+                line = cap['line']\n+                parts = line.split(',', 2)\n+\n+                # Two forms are accepted:\n+                # 1. \"CAP_NAME\" with type/desc as subfields\n+                # 2. \"CAP_NAME, TYPE, description\"\n+                if len(parts) \u003e= 2:\n+                    # Comma-separated form\n+                    cap_name = parts[0].strip()\n+                    cap_type = parts[1].strip()\n+                    cap_desc = parts[2].strip() if len(parts) \u003e 2 else cap.get('desc', cap_name)\n+                else:\n+                    # Subfield form: capability name on main line\n+                    cap_name = line.strip()\n+                    cap_type = cap.get('type', 'KAPI_CAP_PERFORM_OPERATION')\n+                    cap_desc = cap.get('desc', cap_name)\n+\n+                # Map capability type aliases to KAPI_CAP_* enum values.\n+                cap_type_map = {\n+                    'required':             'KAPI_CAP_PERFORM_OPERATION',\n+                    'bypass':               'KAPI_CAP_BYPASS_CHECK',\n+                    'grant':                'KAPI_CAP_GRANT_PERMISSION',\n+                    'override':             'KAPI_CAP_OVERRIDE_RESTRICTION',\n+                    'access':               'KAPI_CAP_ACCESS_RESOURCE',\n+                    'modify':               'KAPI_CAP_MODIFY_BEHAVIOR',\n+                    'limit':                'KAPI_CAP_INCREASE_LIMIT',\n+                    'bypass_check':         'KAPI_CAP_BYPASS_CHECK',\n+                    'increase_limit':       'KAPI_CAP_INCREASE_LIMIT',\n+                    'override_restriction': 'KAPI_CAP_OVERRIDE_RESTRICTION',\n+                    'grant_permission':     'KAPI_CAP_GRANT_PERMISSION',\n+                    'modify_behavior':      'KAPI_CAP_MODIFY_BEHAVIOR',\n+                    'access_resource':      'KAPI_CAP_ACCESS_RESOURCE',\n+                    'perform_operation':    'KAPI_CAP_PERFORM_OPERATION',\n+                }\n+                cap_type = cap_type_map.get(cap_type, cap_type)\n+\n+                # Any BYPASS variant maps to KAPI_CAP_BYPASS_CHECK\n+                if 'BYPASS' in cap_type and cap_type != 'KAPI_CAP_BYPASS_CHECK':\n+                    cap_type = 'KAPI_CAP_BYPASS_CHECK'\n+\n+                # Ensure cap_type is a valid enum\n+                valid_types = [\n+                    'KAPI_CAP_BYPASS_CHECK', 'KAPI_CAP_INCREASE_LIMIT',\n+                    'KAPI_CAP_OVERRIDE_RESTRICTION', 'KAPI_CAP_GRANT_PERMISSION',\n+                    'KAPI_CAP_MODIFY_BEHAVIOR', 'KAPI_CAP_ACCESS_RESOURCE',\n+                    'KAPI_CAP_PERFORM_OPERATION'\n+                ]\n+                if cap_type not in valid_types:\n+                    cap_type = 'KAPI_CAP_PERFORM_OPERATION'\n+\n+                self.data += f\"\\n\\tKAPI_CAPABILITY({idx}, {cap_name}, {self._format_macro_param(cap_desc)}, {cap_type})\\n\"\n+\n+                for key, macro in [\n+                    ('allows', 'KAPI_CAP_ALLOWS'),\n+                    ('without', 'KAPI_CAP_WITHOUT'),\n+                    ('condition', 'KAPI_CAP_CONDITION'),\n+                    ('priority', 'KAPI_CAP_PRIORITY'),\n+                ]:\n+                    if cap.get(key):\n+                        value = self._format_macro_param(cap[key]) if key != 'priority' else cap[key]\n+                        self.data += f\"\\t\\t{macro}({value})\\n\"\n+\n+                self.data += \"\\t},\\n\"\n+\n+    # Skip output methods for non-function types\n+    def out_enum(self, fname, name, args): pass\n+    def out_typedef(self, fname, name, args): pass\n+    def out_struct(self, fname, name, args): pass\n+    def out_doc(self, fname, name, args): pass\ndiff --git a/tools/lib/python/kdoc/kdoc_files.py b/tools/lib/python/kdoc/kdoc_files.py\nindex ed82b6e6ab25b..5b81a1e4cc9e9 100644\n--- a/tools/lib/python/kdoc/kdoc_files.py\n+++ b/tools/lib/python/kdoc/kdoc_files.py\n@@ -94,13 +94,14 @@ class KdocConfig():\n     \"\"\"\n     def __init__(self, verbose=False, werror=False, wreturn=False,\n                  wshort_desc=False, wcontents_before_sections=False,\n-                 logger=None):\n+                 logger=None, apispec=False):\n \n         self.verbose = verbose\n         self.werror = werror\n         self.wreturn = wreturn\n         self.wshort_desc =  wshort_desc\n         self.wcontents_before_sections = wcontents_before_sections\n+        self.apispec = apispec\n \n         if logger:\n             self.log = logger\n@@ -159,6 +160,10 @@ class KernelFiles():\n     ``yaml_content``\n         Defines what will be inside the YAML file.\n \n+    ``apispec``\n+        If True, also parse the kernel API specification sections used\n+        by the ``-apispec`` output. Default: False.\n+\n     Note:\n         There are two type of parsers defined here:\n \n@@ -232,7 +237,8 @@ class KernelFiles():\n     def __init__(self, verbose=False, out_style=None, xforms=None,\n                  werror=False, wreturn=False, wshort_desc=False,\n                  wcontents_before_sections=False,\n-                 yaml_file=None, yaml_content=None, logger=None):\n+                 yaml_file=None, yaml_content=None, logger=None,\n+                 apispec=False):\n         \"\"\"\n         Initialize startup variables and parse all files.\n         \"\"\"\n@@ -271,7 +277,7 @@ class KernelFiles():\n         # used to send control configuration to KernelDoc class. As such,\n         # those variables are read-only inside the KernelDoc.\n         self.config = KdocConfig(verbose, werror, wreturn, wshort_desc,\n-                                 wcontents_before_sections, logger)\n+                                 wcontents_before_sections, logger, apispec)\n \n         # Override log warning, as we want to count errors\n         self.config.warning = self.warning\ndiff --git a/tools/lib/python/kdoc/kdoc_parser.py b/tools/lib/python/kdoc/kdoc_parser.py\nindex d9ad1ddc87dd1..a29fdccda8aee 100644\n--- a/tools/lib/python/kdoc/kdoc_parser.py\n+++ b/tools/lib/python/kdoc/kdoc_parser.py\n@@ -30,6 +30,23 @@ from kdoc.kdoc_item import KdocItem\n # Allow whitespace at end of comment start.\n doc_start = KernRe(r'^/\\*\\*\\s*$', cache=False)\n \n+# Sections that are allowed to be duplicated for API specifications\n+# These represent lists of items (multiple errors, signals, etc.)\n+ALLOWED_DUPLICATE_SECTIONS = {\n+    'param', '@param',\n+    'error', '@error',\n+    'signal', '@signal',\n+    'lock', '@lock',\n+    'side-effect', '@side-effect',\n+    'state-trans', '@state-trans',\n+    'capability', '@capability',\n+    'constraint', '@constraint',\n+    'validation-group', '@validation-group',\n+    'validation-rule', '@validation-rule',\n+    'validation-flag', '@validation-flag',\n+    'struct-field', '@struct-field',\n+}\n+\n doc_end = KernRe(r'\\*/', cache=False)\n doc_com = KernRe(r'\\s*\\*\\s*', cache=False)\n doc_com_body = KernRe(r'\\s*\\* ?', cache=False)\n@@ -48,6 +65,71 @@ doc_sect = doc_com + \\\n     KernRe(r'\\s*(@[.\\w]+|@\\.\\.\\.|' + known_section_names + r')\\s*:([^:].*)?$',\n            flags=re.I, cache=False)\n \n+# API specification section names (for KAPI spec framework), only\n+# recognized when generating -apispec output\n+# Format: (base_name, has_count_variant, has_other_variants)\n+# Sections with has_count_variant=True need negative lookahead in kapi_doc_sect\n+# to avoid matching 'error' when 'error-count' is intended\n+_kapi_base_sections = [\n+    # (name, needs_lookahead, additional_variants)\n+    ('api-type', False, []),\n+    ('api-version', False, []),\n+    ('param', True, []),  # has param-count\n+    ('struct', True, ['struct-type', 'struct-field', 'struct-field-[a-z\\\\-]+']),\n+    ('validation-group', False, []),\n+    ('validation-policy', False, []),\n+    ('validation-flag', False, []),\n+    ('validation-rule', False, []),\n+    ('error', True, ['error-code', 'error-condition']),\n+    ('capability', True, []),\n+    ('signal', True, []),\n+    ('lock', True, []),\n+    ('context-flags', False, []),\n+    ('contexts', False, []),\n+    ('return', True, ['return-type', 'return-check', 'return-check-type',\n+                      'return-success', 'return-desc']),\n+    ('long-desc', False, []),\n+    ('constraint', True, []),\n+    ('side-effect', True, []),\n+    ('state-trans', True, []),\n+]\n+\n+def _build_kapi_patterns():\n+    \"\"\"Build KAPI section patterns from the base definitions.\"\"\"\n+    validation_parts = []  # For kapi_known_sections (simple validation)\n+    parsing_parts = []     # For kapi_doc_sect (with negative lookaheads)\n+\n+    for name, has_count, variants in _kapi_base_sections:\n+        # Add base name (with optional @ prefix)\n+        validation_parts.append(f'@?{name}')\n+        if has_count:\n+            # Need negative lookahead to not match 'name-count' or 'name-*'\n+            parsing_parts.append(f'@?{name}(?!-)')\n+            validation_parts.append(f'@?{name}-count')\n+            parsing_parts.append(f'@?{name}-count')\n+        else:\n+            parsing_parts.append(f'@?{name}')\n+\n+        # Add variants\n+        for variant in variants:\n+            validation_parts.append(f'@?{variant}')\n+            parsing_parts.append(f'@?{variant}')\n+\n+    # Add catch-all for kapi-* extensions\n+    validation_parts.append(r'@?kapi-.*')\n+    parsing_parts.append(r'@?kapi-.*')\n+\n+    return '|'.join(validation_parts), '|'.join(parsing_parts)\n+\n+_kapi_validation_pattern, _kapi_parsing_pattern = _build_kapi_patterns()\n+\n+kapi_known_sections = KernRe(known_section_names + '|' +\n+                             _kapi_validation_pattern, flags=re.I)\n+kapi_doc_sect = doc_com + \\\n+    KernRe(r'\\s*(@[.\\w\\-]+|@\\.\\.\\.|' + known_section_names + '|' +\n+           _kapi_parsing_pattern + r')\\s*:([^:].*)?$',\n+           flags=re.I, cache=False)\n+\n doc_content = doc_com_body + KernRe(r'(.*)', cache=False)\n doc_inline_start = KernRe(r'^\\s*/\\*\\*\\s*$', cache=False)\n doc_inline_sect = KernRe(r'\\s*\\*\\s*(@\\s*[\\w][\\w\\.]*\\s*):(.*)', cache=False)\n@@ -214,7 +296,9 @@ class KernelEntry:\n         else:\n             if name in self.sections and self.sections[name] != \"\":\n                 # Only warn on user-specified duplicate section names\n-                if name != SECTION_DEFAULT:\n+                # Skip warning for sections that are expected to have duplicates\n+                # (like error, param, signal, etc. for API specifications)\n+                if name != SECTION_DEFAULT and name not in ALLOWED_DUPLICATE_SECTIONS:\n                     self.emit_msg(self.new_start_line,\n                                   f\"duplicate section name '{name}'\")\n                 # Treat as a new paragraph - add a blank line\n@@ -255,6 +339,13 @@ class KernelDoc:\n         self.xforms = xforms\n         self.store_src = store_src\n \n+        if self.config.apispec:\n+            self.known_sections = kapi_known_sections\n+            self.doc_sect = kapi_doc_sect\n+        else:\n+            self.known_sections = known_sections\n+            self.doc_sect = doc_sect\n+\n         tokenizer_set_log(self.config.log, f\"{self.fname}: CMatch: \")\n \n         # Initial state for the state machines\n@@ -611,7 +702,7 @@ class KernelDoc:\n         \"\"\"\n         for section in self.entry.sections:\n             if section not in self.entry.parameterlist and \\\n-               not known_sections.search(section):\n+               not self.known_sections.search(section):\n                 hint = self.get_suggestions_hint(section, self.entry.parameterlist)\n                 if decl_type == 'function':\n                     dname = f\"{decl_type} parameter\"\n@@ -1314,12 +1405,12 @@ class KernelDoc:\n         \"\"\"\n         Helper function to determine if a new section is being started.\n         \"\"\"\n-        if doc_sect.search(line):\n+        if self.doc_sect.search(line):\n             self.state = state.BODY\n             #\n             # Pick out the name of our new section, tweaking it if need be.\n             #\n-            newsection = doc_sect.group(1)\n+            newsection = self.doc_sect.group(1)\n             if newsection.lower() == 'description':\n                 newsection = 'Description'\n             elif newsection.lower() == 'context':\n@@ -1334,7 +1425,7 @@ class KernelDoc:\n             #\n             # Initialize the contents, and get the new section going.\n             #\n-            newcontents = doc_sect.group(2)\n+            newcontents = self.doc_sect.group(2)\n             if not newcontents:\n                 newcontents = \"\"\n             self.dump_section()\ndiff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile\nindex 273853937c254..d5f973eeefc83 100644\n--- a/tools/testing/selftests/Makefile\n+++ b/tools/testing/selftests/Makefile\n@@ -60,6 +60,7 @@ TARGETS += intel_pstate\n TARGETS += iommu\n TARGETS += ipc\n TARGETS += ir\n+TARGETS += kapi\n TARGETS += kcmp\n TARGETS += kexec\n TARGETS += kselftest_harness\ndiff --git a/tools/testing/selftests/kapi/.gitignore b/tools/testing/selftests/kapi/.gitignore\nnew file mode 100644\nindex 0000000000000..e60893d183d81\n--- /dev/null\n+++ b/tools/testing/selftests/kapi/.gitignore\n@@ -0,0 +1,2 @@\n+# SPDX-License-Identifier: GPL-2.0-only\n+test_kapi\ndiff --git a/tools/testing/selftests/kapi/Makefile b/tools/testing/selftests/kapi/Makefile\nnew file mode 100644\nindex 0000000000000..d1bb26541c0b8\n--- /dev/null\n+++ b/tools/testing/selftests/kapi/Makefile\n@@ -0,0 +1,7 @@\n+# SPDX-License-Identifier: GPL-2.0\n+\n+TEST_GEN_PROGS := test_kapi\n+\n+CFLAGS += -static -Wall -Wextra -O2 $(KHDR_INCLUDES)\n+\n+include ../lib.mk\ndiff --git a/tools/testing/selftests/kapi/kapi_test_util.h b/tools/testing/selftests/kapi/kapi_test_util.h\nnew file mode 100644\nindex 0000000000000..a09871207bfc6\n--- /dev/null\n+++ b/tools/testing/selftests/kapi/kapi_test_util.h\n@@ -0,0 +1,31 @@\n+/* SPDX-License-Identifier: GPL-2.0 */\n+/*\n+ * Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+ *\n+ * Compatibility helpers for KAPI selftests.\n+ *\n+ * __NR_open does not exist on architectures using the generic syscall table\n+ * (e.g. arm64, riscv); only __NR_openat does. There the wrapper falls back to\n+ * openat(AT_FDCWD, ...), which does not go through sys_open, so the sys_open\n+ * KAPI checks are not exercised.\n+ */\n+#ifndef KAPI_TEST_UTIL_H\n+#define KAPI_TEST_UTIL_H\n+\n+#include \u003cfcntl.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003csys/syscall.h\u003e\n+\n+#ifndef __NR_open\n+static inline long kapi_sys_open(const char *pathname, int flags, int mode)\n+{\n+\treturn syscall(__NR_openat, AT_FDCWD, pathname, flags, mode);\n+}\n+#else\n+static inline long kapi_sys_open(const char *pathname, int flags, int mode)\n+{\n+\treturn syscall(__NR_open, pathname, flags, mode);\n+}\n+#endif\n+\n+#endif /* KAPI_TEST_UTIL_H */\ndiff --git a/tools/testing/selftests/kapi/test_kapi.c b/tools/testing/selftests/kapi/test_kapi.c\nnew file mode 100644\nindex 0000000000000..deccb075ee1f9\n--- /dev/null\n+++ b/tools/testing/selftests/kapi/test_kapi.c\n@@ -0,0 +1,1163 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Copyright (C) 2026 Sasha Levin \u003csashal@kernel.org\u003e\n+ *\n+ * Userspace selftest for KAPI runtime verification of syscall parameters.\n+ *\n+ * Exercises sys_open, sys_read, sys_write, and sys_close through raw\n+ * syscall() to ensure KAPI pre-validation wrappers interact correctly\n+ * with normal kernel error handling.\n+ *\n+ * Requires CONFIG_KAPI_RUNTIME_CHECKS=y for full coverage; many tests\n+ * also pass without it.\n+ *\n+ * TAP output format.\n+ */\n+\n+#define _GNU_SOURCE\n+#include \u003cstdbool.h\u003e\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003cfcntl.h\u003e\n+#include \u003cerrno.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003csys/syscall.h\u003e\n+#include \u003csys/stat.h\u003e\n+#include \u003clinux/limits.h\u003e\n+#include \"../kselftest.h\"\n+#include \"kapi_test_util.h\"\n+\n+#define NUM_TESTS 31\n+\n+/* Set from the SIGPIPE handler. */\n+static volatile sig_atomic_t got_sigpipe;\n+\n+static void tap_ok(const char *desc)\n+{\n+\tksft_test_result_pass(\"%s\\n\", desc);\n+}\n+\n+static void tap_fail(const char *desc, const char *reason)\n+{\n+\tksft_test_result_fail(\"%s: %s\\n\", desc, reason);\n+}\n+\n+static void tap_skip(const char *desc, const char *reason)\n+{\n+\tksft_test_result_skip(\"%s: %s\\n\", desc, reason);\n+}\n+\n+/*\n+ * Return true when CONFIG_KAPI_RUNTIME_CHECKS is active: it rejects unknown\n+ * open flag bits with EINVAL, while the kernel otherwise ignores them.\n+ */\n+static bool kapi_runtime_checks_active(void)\n+{\n+\tlong ret;\n+\n+\terrno = 0;\n+\tret = kapi_sys_open(\"/dev/null\", 0x10000000, 0);\n+\tif (ret \u003e= 0) {\n+\t\tsyscall(__NR_close, (int)ret);\n+\t\treturn false;\n+\t}\n+\n+\treturn errno == EINVAL;\n+}\n+\n+static void sigpipe_handler(int sig)\n+{\n+\t(void)sig;\n+\tgot_sigpipe = 1;\n+}\n+\n+/* ---- Valid operation tests ---- */\n+\n+/*\n+ * Test 1: open a readable file\n+ * Returns fd on success.\n+ */\n+static int test_open_valid(void)\n+{\n+\terrno = 0;\n+\tlong fd = kapi_sys_open(\"/etc/hostname\", O_RDONLY, 0);\n+\n+\tif (fd \u003e= 0) {\n+\t\ttap_ok(\"open valid file\");\n+\t} else {\n+\t\t/* /etc/hostname might not exist; try /etc/passwd */\n+\t\terrno = 0;\n+\t\tfd = kapi_sys_open(\"/etc/passwd\", O_RDONLY, 0);\n+\t\tif (fd \u003e= 0)\n+\t\t\ttap_ok(\"open valid file (fallback /etc/passwd)\");\n+\t\telse\n+\t\t\ttap_fail(\"open valid file\", strerror(errno));\n+\t}\n+\treturn (int)fd;\n+}\n+\n+/*\n+ * Test 2: read from fd\n+ */\n+static void test_read_valid(int fd)\n+{\n+\tchar buf[256];\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_read, fd, buf, sizeof(buf));\n+\n+\tif (ret \u003e 0)\n+\t\ttap_ok(\"read from valid fd\");\n+\telse if (ret == 0)\n+\t\ttap_ok(\"read from valid fd (EOF)\");\n+\telse\n+\t\ttap_fail(\"read from valid fd\", strerror(errno));\n+}\n+\n+/*\n+ * Test 3: write to /dev/null\n+ */\n+static void test_write_valid(void)\n+{\n+\terrno = 0;\n+\tlong devnull = kapi_sys_open(\"/dev/null\", O_WRONLY, 0);\n+\n+\tif (devnull \u003c 0) {\n+\t\ttap_fail(\"write to /dev/null (open failed)\", strerror(errno));\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_write, (int)devnull, \"hello\", 5);\n+\n+\tif (ret == 5)\n+\t\ttap_ok(\"write to /dev/null\");\n+\telse\n+\t\ttap_fail(\"write to /dev/null\",\n+\t\t\t ret \u003c 0 ? strerror(errno) : \"short write\");\n+\n+\tsyscall(__NR_close, (int)devnull);\n+}\n+\n+/*\n+ * Test 4: close fd\n+ */\n+static void test_close_valid(int fd)\n+{\n+\terrno = 0;\n+\tlong ret = syscall(__NR_close, fd);\n+\n+\tif (ret == 0)\n+\t\ttap_ok(\"close valid fd\");\n+\telse\n+\t\ttap_fail(\"close valid fd\", strerror(errno));\n+}\n+\n+/* ---- KAPI parameter rejection tests ---- */\n+\n+/*\n+ * Test 5: open with invalid flag bits\n+ * 0x10000000 is outside the valid O_* mask, KAPI should reject.\n+ */\n+static void test_open_invalid_flags(void)\n+{\n+\tlong ret;\n+\n+\tif (!kapi_runtime_checks_active()) {\n+\t\ttap_skip(\"open with invalid flags\",\n+\t\t\t \"CONFIG_KAPI_RUNTIME_CHECKS not enabled\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\t/*\n+\t * Use /dev/null (always present on any sane rootfs) so KAPI's flag\n+\t * validation is reached before a path-lookup ENOENT can mask it.\n+\t * 0x10000000 is outside the valid O_* mask.\n+\t */\n+\tret = kapi_sys_open(\"/dev/null\", 0x10000000, 0);\n+\n+\tif (ret == -1 \u0026\u0026 errno == EINVAL) {\n+\t\ttap_ok(\"open with invalid flags returns EINVAL\");\n+\t} else if (ret \u003e= 0) {\n+\t\ttap_fail(\"open with invalid flags\", \"expected EINVAL, got success\");\n+\t\tsyscall(__NR_close, (int)ret);\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected EINVAL, got %s\",\n+\t\t\t strerror(errno));\n+\t\ttap_fail(\"open with invalid flags\", msg);\n+\t}\n+}\n+\n+/*\n+ * Test 6: open with mode bits above S_IALLUGO\n+ * The kernel masks mode with S_IALLUGO (07777), so KAPI must accept it.\n+ */\n+static void test_open_high_mode_bits(void)\n+{\n+\tconst char *desc = \"open with mode bits above 07777 creates file\";\n+\tchar path[64];\n+\tstruct stat st;\n+\tmode_t um, want;\n+\tlong fd;\n+\n+\tsnprintf(path, sizeof(path), \"/tmp/kapi_test_mode.%d\", getpid());\n+\tunlink(path);\n+\n+\tum = umask(0);\n+\tumask(um);\n+\twant = 07777 \u0026 ~um;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(path, O_CREAT | O_WRONLY | O_EXCL, 0xFFFF);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(desc, strerror(errno));\n+\t\treturn;\n+\t}\n+\n+\tif (fstat((int)fd, \u0026st) \u003c 0) {\n+\t\ttap_fail(desc, strerror(errno));\n+\t} else if ((st.st_mode \u0026 07777) != want) {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected mode %04o, got %04o\",\n+\t\t\t (unsigned int)want, (unsigned int)(st.st_mode \u0026 07777));\n+\t\ttap_fail(desc, msg);\n+\t} else {\n+\t\ttap_ok(desc);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+\tunlink(path);\n+}\n+\n+/*\n+ * Test 7: open with NULL path\n+ * KAPI USER_PATH constraint should reject NULL.\n+ */\n+static void test_open_null_path(void)\n+{\n+\terrno = 0;\n+\tlong ret = kapi_sys_open(NULL, O_RDONLY, 0);\n+\n+\tif (ret == -1 \u0026\u0026 errno == EINVAL) {\n+\t\ttap_ok(\"open with NULL path returns EINVAL\");\n+\t} else if (ret == -1 \u0026\u0026 errno == EFAULT) {\n+\t\t/* Kernel may catch this as EFAULT before KAPI */\n+\t\ttap_ok(\"open with NULL path returns EFAULT (acceptable)\");\n+\t} else if (ret \u003e= 0) {\n+\t\ttap_fail(\"open with NULL path\", \"expected error, got success\");\n+\t\tsyscall(__NR_close, (int)ret);\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"got %s\", strerror(errno));\n+\t\ttap_fail(\"open with NULL path\", msg);\n+\t}\n+}\n+\n+/*\n+ * Test 8: open with flag bit 30 set (0x40000000)\n+ * This bit is outside the valid O_* mask, KAPI should reject with EINVAL.\n+ */\n+static void test_open_flag_bit30(void)\n+{\n+\tlong ret;\n+\n+\tif (!kapi_runtime_checks_active()) {\n+\t\ttap_skip(\"open with flag bit 30 (0x40000000) returns EINVAL\",\n+\t\t\t \"CONFIG_KAPI_RUNTIME_CHECKS not enabled\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tret = kapi_sys_open(\"/dev/null\", 0x40000000, 0);\n+\n+\tif (ret == -1 \u0026\u0026 errno == EINVAL) {\n+\t\ttap_ok(\"open with flag bit 30 (0x40000000) returns EINVAL\");\n+\t} else if (ret \u003e= 0) {\n+\t\ttap_fail(\"open with flag bit 30 (0x40000000) returns EINVAL\",\n+\t\t\t \"expected EINVAL, got success\");\n+\t\tsyscall(__NR_close, (int)ret);\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected EINVAL, got %s\",\n+\t\t\t strerror(errno));\n+\t\ttap_fail(\"open with flag bit 30 (0x40000000) returns EINVAL\",\n+\t\t\t msg);\n+\t}\n+}\n+\n+/* ---- Boundary condition and error path tests ---- */\n+\n+/*\n+ * Test 9: read with fd=-1 should return an error.\n+ * With CONFIG_KAPI_RUNTIME_CHECKS=y, KAPI validates the fd first and\n+ * rejects negative fds (other than AT_FDCWD) with EINVAL.  Without\n+ * KAPI, the kernel returns EBADF.  Accept either.\n+ */\n+static void test_read_bad_fd(void)\n+{\n+\tchar buf[16];\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_read, -1, buf, sizeof(buf));\n+\n+\tif (ret == -1 \u0026\u0026 (errno == EBADF || errno == EINVAL)) {\n+\t\ttap_ok(\"read with fd=-1 returns error\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected EBADF/EINVAL, got %s\",\n+\t\t\t ret \u003e= 0 ? \"success\" : strerror(errno));\n+\t\ttap_fail(\"read with fd=-1 returns error\", msg);\n+\t}\n+}\n+\n+/*\n+ * Test 10: read with count=0 should return 0\n+ */\n+static void test_read_zero_count(void)\n+{\n+\tchar buf[1];\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_RDONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"read with count=0 returns 0\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_read, (int)fd, buf, 0);\n+\n+\tif (ret == 0) {\n+\t\ttap_ok(\"read with count=0 returns 0\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected 0, got %ld (errno=%s)\",\n+\t\t\t ret, strerror(errno));\n+\t\ttap_fail(\"read with count=0 returns 0\", msg);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+}\n+\n+/*\n+ * Test 11: write with count=0 should return 0\n+ */\n+static void test_write_zero_count(void)\n+{\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_WRONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"write with count=0 returns 0\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_write, (int)fd, \"x\", 0);\n+\n+\tif (ret == 0) {\n+\t\ttap_ok(\"write with count=0 returns 0\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected 0, got %ld (errno=%s)\",\n+\t\t\t ret, strerror(errno));\n+\t\ttap_fail(\"write with count=0 returns 0\", msg);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+}\n+\n+/*\n+ * Test 12: open with a path longer than PATH_MAX should fail\n+ * Expect ENAMETOOLONG or EINVAL.\n+ */\n+static void test_open_long_path(void)\n+{\n+\tchar *longpath;\n+\tsize_t len = PATH_MAX + 256;\n+\n+\tlongpath = malloc(len);\n+\tif (!longpath) {\n+\t\ttap_fail(\"open with path \u003e PATH_MAX\", \"malloc failed\");\n+\t\treturn;\n+\t}\n+\n+\tmemset(longpath, 'A', len - 1);\n+\tlongpath[0] = '/';\n+\tlongpath[len - 1] = '\\0';\n+\n+\terrno = 0;\n+\tlong ret = kapi_sys_open(longpath, O_RDONLY, 0);\n+\n+\tif (ret == -1 \u0026\u0026 (errno == ENAMETOOLONG || errno == EINVAL)) {\n+\t\ttap_ok(\"open with path \u003e PATH_MAX returns ENAMETOOLONG/EINVAL\");\n+\t} else if (ret \u003e= 0) {\n+\t\ttap_fail(\"open with path \u003e PATH_MAX\",\n+\t\t\t \"expected error, got success\");\n+\t\tsyscall(__NR_close, (int)ret);\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg),\n+\t\t\t \"expected ENAMETOOLONG/EINVAL, got %s\",\n+\t\t\t strerror(errno));\n+\t\ttap_fail(\"open with path \u003e PATH_MAX\", msg);\n+\t}\n+\n+\tfree(longpath);\n+}\n+\n+/*\n+ * Test 13: read with unmapped user pointer should return EFAULT or EINVAL.\n+ * Use a pipe with data so the kernel actually tries to copy to the buffer.\n+ */\n+static void test_read_unmapped_buf(void)\n+{\n+\tint pipefd[2];\n+\n+\tif (pipe(pipefd) \u003c 0) {\n+\t\ttap_fail(\"read with unmapped buffer returns EFAULT/EINVAL\",\n+\t\t\t \"pipe() failed\");\n+\t\treturn;\n+\t}\n+\n+\t/* Write some data so read has something to copy */\n+\tif (write(pipefd[1], \"hello\", 5) != 5) {\n+\t\ttap_fail(\"read with unmapped buffer returns EFAULT/EINVAL\",\n+\t\t\t \"pipe write failed\");\n+\t\tclose(pipefd[0]);\n+\t\tclose(pipefd[1]);\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_read, pipefd[0], (void *)0xDEAD0000, 16);\n+\n+\tif (ret == -1 \u0026\u0026 (errno == EFAULT || errno == EINVAL)) {\n+\t\ttap_ok(\"read with unmapped buffer returns EFAULT/EINVAL\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg),\n+\t\t\t \"expected EFAULT/EINVAL, got %s\",\n+\t\t\t ret \u003e= 0 ? \"success\" : strerror(errno));\n+\t\ttap_fail(\"read with unmapped buffer returns EFAULT/EINVAL\",\n+\t\t\t msg);\n+\t}\n+\n+\tclose(pipefd[0]);\n+\tclose(pipefd[1]);\n+}\n+\n+/*\n+ * Test 14: write with unmapped user pointer should return EFAULT or EINVAL.\n+ * Use a pipe so the kernel actually tries to copy from the buffer.\n+ */\n+static void test_write_unmapped_buf(void)\n+{\n+\tint pipefd[2];\n+\n+\tif (pipe(pipefd) \u003c 0) {\n+\t\ttap_fail(\"write with unmapped buffer returns EFAULT/EINVAL\",\n+\t\t\t \"pipe() failed\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_write, pipefd[1], (void *)0xDEAD0000, 16);\n+\n+\tif (ret == -1 \u0026\u0026 (errno == EFAULT || errno == EINVAL)) {\n+\t\ttap_ok(\"write with unmapped buffer returns EFAULT/EINVAL\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg),\n+\t\t\t \"expected EFAULT/EINVAL, got %s\",\n+\t\t\t ret \u003e= 0 ? \"success\" : strerror(errno));\n+\t\ttap_fail(\"write with unmapped buffer returns EFAULT/EINVAL\",\n+\t\t\t msg);\n+\t}\n+\n+\tclose(pipefd[0]);\n+\tclose(pipefd[1]);\n+}\n+\n+/*\n+ * Test 15: close an already-closed fd should return EBADF\n+ */\n+static void test_close_already_closed(void)\n+{\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_RDONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"close already-closed fd returns EBADF\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\t/* Close it once - should succeed */\n+\tsyscall(__NR_close, (int)fd);\n+\n+\t/* Close it again - should fail with EBADF */\n+\terrno = 0;\n+\tlong ret = syscall(__NR_close, (int)fd);\n+\n+\tif (ret == -1 \u0026\u0026 errno == EBADF) {\n+\t\ttap_ok(\"close already-closed fd returns EBADF\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected EBADF, got %s\",\n+\t\t\t ret == 0 ? \"success\" : strerror(errno));\n+\t\ttap_fail(\"close already-closed fd returns EBADF\", msg);\n+\t}\n+}\n+\n+/*\n+ * Test 16: open /dev/null with O_RDONLY|O_CLOEXEC should succeed\n+ */\n+static void test_open_valid_cloexec(void)\n+{\n+\terrno = 0;\n+\tlong fd = kapi_sys_open(\"/dev/null\", O_RDONLY | O_CLOEXEC, 0);\n+\n+\tif (fd \u003e= 0) {\n+\t\ttap_ok(\"open /dev/null with O_RDONLY|O_CLOEXEC succeeds\");\n+\t\tsyscall(__NR_close, (int)fd);\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected success, got %s\",\n+\t\t\t strerror(errno));\n+\t\ttap_fail(\"open /dev/null with O_RDONLY|O_CLOEXEC succeeds\",\n+\t\t\t msg);\n+\t}\n+}\n+\n+/*\n+ * Test 17: write 0 bytes to /dev/null should return 0\n+ */\n+static void test_write_zero_devnull(void)\n+{\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_WRONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"write 0 bytes to /dev/null returns 0\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_write, (int)fd, \"\", 0);\n+\n+\tif (ret == 0) {\n+\t\ttap_ok(\"write 0 bytes to /dev/null returns 0\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected 0, got %ld (errno=%s)\",\n+\t\t\t ret, strerror(errno));\n+\t\ttap_fail(\"write 0 bytes to /dev/null returns 0\", msg);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+}\n+\n+/*\n+ * Test 18: read from a write-only fd should return EBADF\n+ */\n+static void test_read_writeonly_fd(void)\n+{\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_WRONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"read from write-only fd returns EBADF\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\tchar buf[16];\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));\n+\n+\tif (ret == -1 \u0026\u0026 errno == EBADF) {\n+\t\ttap_ok(\"read from write-only fd returns EBADF\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected EBADF, got %s\",\n+\t\t\t ret \u003e= 0 ? \"success\" : strerror(errno));\n+\t\ttap_fail(\"read from write-only fd returns EBADF\", msg);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+}\n+\n+/*\n+ * Test 19: write to a read-only fd should return EBADF\n+ */\n+static void test_write_readonly_fd(void)\n+{\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_RDONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"write to read-only fd returns EBADF\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_write, (int)fd, \"hello\", 5);\n+\n+\tif (ret == -1 \u0026\u0026 errno == EBADF) {\n+\t\ttap_ok(\"write to read-only fd returns EBADF\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected EBADF, got %s\",\n+\t\t\t ret \u003e= 0 ? \"success\" : strerror(errno));\n+\t\ttap_fail(\"write to read-only fd returns EBADF\", msg);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+}\n+\n+/*\n+ * Test 20: close fd 9999 (likely invalid) should return EBADF\n+ */\n+static void test_close_fd_9999(void)\n+{\n+\terrno = 0;\n+\tlong ret = syscall(__NR_close, 9999);\n+\n+\tif (ret == -1 \u0026\u0026 errno == EBADF) {\n+\t\ttap_ok(\"close fd 9999 returns EBADF\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected EBADF, got %s\",\n+\t\t\t ret == 0 ? \"success\" : strerror(errno));\n+\t\ttap_fail(\"close fd 9999 returns EBADF\", msg);\n+\t}\n+}\n+\n+/*\n+ * Test 21: read from pipe after write end is closed returns 0 (EOF)\n+ */\n+static void test_read_closed_pipe(void)\n+{\n+\tint pipefd[2];\n+\n+\tif (pipe(pipefd) \u003c 0) {\n+\t\ttap_fail(\"read from closed pipe returns 0 (EOF)\",\n+\t\t\t \"pipe() failed\");\n+\t\treturn;\n+\t}\n+\n+\t/* Close write end */\n+\tclose(pipefd[1]);\n+\n+\tchar buf[16];\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_read, pipefd[0], buf, sizeof(buf));\n+\n+\tif (ret == 0) {\n+\t\ttap_ok(\"read from closed pipe returns 0 (EOF)\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected 0, got %ld (errno=%s)\",\n+\t\t\t ret, ret \u003c 0 ? strerror(errno) : \"n/a\");\n+\t\ttap_fail(\"read from closed pipe returns 0 (EOF)\", msg);\n+\t}\n+\n+\tclose(pipefd[0]);\n+}\n+\n+/*\n+ * Test 22: write to pipe after read end is closed returns EPIPE + SIGPIPE\n+ */\n+static void test_write_closed_pipe(void)\n+{\n+\tint pipefd[2];\n+\tstruct sigaction sa, old_sa;\n+\n+\tif (pipe(pipefd) \u003c 0) {\n+\t\ttap_fail(\"write to closed pipe returns EPIPE + SIGPIPE\",\n+\t\t\t \"pipe() failed\");\n+\t\treturn;\n+\t}\n+\n+\t/* Install SIGPIPE handler */\n+\tmemset(\u0026sa, 0, sizeof(sa));\n+\tsa.sa_handler = sigpipe_handler;\n+\tsigemptyset(\u0026sa.sa_mask);\n+\tsigaction(SIGPIPE, \u0026sa, \u0026old_sa);\n+\n+\tgot_sigpipe = 0;\n+\n+\t/* Close read end */\n+\tclose(pipefd[0]);\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_write, pipefd[1], \"hello\", 5);\n+\n+\tif (ret == -1 \u0026\u0026 errno == EPIPE \u0026\u0026 got_sigpipe) {\n+\t\ttap_ok(\"write to closed pipe returns EPIPE + SIGPIPE\");\n+\t} else if (ret == -1 \u0026\u0026 errno == EPIPE) {\n+\t\ttap_ok(\"write to closed pipe returns EPIPE (SIGPIPE not caught)\");\n+\t} else {\n+\t\tchar msg[128];\n+\n+\t\tsnprintf(msg, sizeof(msg),\n+\t\t\t \"expected EPIPE, got %s (sigpipe=%d)\",\n+\t\t\t ret \u003e= 0 ? \"success\" : strerror(errno),\n+\t\t\t (int)got_sigpipe);\n+\t\ttap_fail(\"write to closed pipe returns EPIPE + SIGPIPE\", msg);\n+\t}\n+\n+\t/* Restore SIGPIPE handler */\n+\tsigaction(SIGPIPE, \u0026old_sa, NULL);\n+\tclose(pipefd[1]);\n+}\n+\n+/*\n+ * Test 23: open with O_DIRECTORY on a non-directory returns ENOTDIR\n+ */\n+static void test_open_directory_on_file(void)\n+{\n+\terrno = 0;\n+\tlong ret = kapi_sys_open(\"/dev/null\", O_RDONLY | O_DIRECTORY, 0);\n+\n+\tif (ret == -1 \u0026\u0026 errno == ENOTDIR) {\n+\t\ttap_ok(\"open O_DIRECTORY on non-directory returns ENOTDIR\");\n+\t} else if (ret \u003e= 0) {\n+\t\ttap_fail(\"open O_DIRECTORY on non-directory\",\n+\t\t\t \"expected ENOTDIR, got success\");\n+\t\tsyscall(__NR_close, (int)ret);\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected ENOTDIR, got %s\",\n+\t\t\t strerror(errno));\n+\t\ttap_fail(\"open O_DIRECTORY on non-directory\", msg);\n+\t}\n+}\n+\n+/*\n+ * Test 24: open nonexistent file without O_CREAT returns ENOENT\n+ */\n+static void test_open_nonexistent(void)\n+{\n+\terrno = 0;\n+\tlong ret = kapi_sys_open(\"/tmp/kapi_nonexistent_file_12345\",\n+\t\t\t\t O_RDONLY, 0);\n+\n+\tif (ret == -1 \u0026\u0026 errno == ENOENT) {\n+\t\ttap_ok(\"open nonexistent file without O_CREAT returns ENOENT\");\n+\t} else if (ret \u003e= 0) {\n+\t\ttap_fail(\"open nonexistent file\",\n+\t\t\t \"expected ENOENT, got success (file exists?)\");\n+\t\tsyscall(__NR_close, (int)ret);\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected ENOENT, got %s\",\n+\t\t\t strerror(errno));\n+\t\ttap_fail(\"open nonexistent file\", msg);\n+\t}\n+}\n+\n+/*\n+ * Test 25: close stdin (fd 0) should succeed\n+ * We dup it first so we can restore it.\n+ */\n+static void test_close_stdin(void)\n+{\n+\tint saved_stdin = dup(0);\n+\n+\tif (saved_stdin \u003c 0) {\n+\t\ttap_fail(\"close stdin succeeds\", \"cannot dup stdin\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_close, 0);\n+\n+\tif (ret == 0) {\n+\t\ttap_ok(\"close stdin (fd 0) succeeds\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected success, got %s\",\n+\t\t\t strerror(errno));\n+\t\ttap_fail(\"close stdin (fd 0) succeeds\", msg);\n+\t}\n+\n+\t/* Restore stdin */\n+\tdup2(saved_stdin, 0);\n+\tclose(saved_stdin);\n+}\n+\n+/*\n+ * Test 26: read after close returns EBADF\n+ */\n+static void test_read_after_close(void)\n+{\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_RDONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"read after close returns EBADF\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+\n+\tchar buf[16];\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));\n+\n+\tif (ret == -1 \u0026\u0026 errno == EBADF) {\n+\t\ttap_ok(\"read after close returns EBADF\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected EBADF, got %s\",\n+\t\t\t ret \u003e= 0 ? \"success\" : strerror(errno));\n+\t\ttap_fail(\"read after close returns EBADF\", msg);\n+\t}\n+}\n+\n+/*\n+ * Test 27: write with large count\n+ * Without KAPI: the kernel clamps count to MAX_RW_COUNT and succeeds.\n+ * With KAPI: KAPI validates the buffer against the count and may\n+ * return EFAULT/EINVAL since the buffer is smaller than count.\n+ * Accept either success or EFAULT/EINVAL.\n+ */\n+static void test_write_large_count(void)\n+{\n+\tlong fd;\n+\tchar buf[64] = \"test data\";\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_WRONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"write with large count handled correctly\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_write, (int)fd, buf, (size_t)0x7ffff000UL);\n+\n+\tif (ret \u003e 0) {\n+\t\ttap_ok(\"write with large count succeeds (clamped, no KAPI)\");\n+\t} else if (ret == -1 \u0026\u0026 (errno == EFAULT || errno == EINVAL)) {\n+\t\ttap_ok(\"write with large count returns EFAULT/EINVAL (KAPI validates buffer)\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected success or EFAULT, got %s\",\n+\t\t\t ret == 0 ? \"zero\" : strerror(errno));\n+\t\ttap_fail(\"write with large count handled correctly\", msg);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+}\n+\n+/* ---- Integration tests ---- */\n+\n+/*\n+ * Test 28: full normal syscall path - open, read, write, close\n+ * Verify KAPI does not interfere with normal operations.\n+ */\n+static void test_normal_path(void)\n+{\n+\tlong rd_fd, wr_fd;\n+\tchar buf[128];\n+\tint ok = 1;\n+\tchar reason[128] = \"\";\n+\n+\t/* Open a readable file */\n+\terrno = 0;\n+\trd_fd = kapi_sys_open(\"/etc/hostname\", O_RDONLY, 0);\n+\tif (rd_fd \u003c 0) {\n+\t\terrno = 0;\n+\t\trd_fd = kapi_sys_open(\"/etc/passwd\", O_RDONLY, 0);\n+\t}\n+\tif (rd_fd \u003c 0) {\n+\t\tsnprintf(reason, sizeof(reason), \"open readable file: %s\",\n+\t\t\t strerror(errno));\n+\t\tok = 0;\n+\t}\n+\n+\t/* Read from it */\n+\tif (ok) {\n+\t\terrno = 0;\n+\t\tlong n = syscall(__NR_read, (int)rd_fd, buf, sizeof(buf));\n+\n+\t\tif (n \u003c 0) {\n+\t\t\tsnprintf(reason, sizeof(reason), \"read: %s\",\n+\t\t\t\t strerror(errno));\n+\t\t\tok = 0;\n+\t\t}\n+\t}\n+\n+\t/* Open /dev/null for writing */\n+\twr_fd = -1;\n+\tif (ok) {\n+\t\terrno = 0;\n+\t\twr_fd = kapi_sys_open(\"/dev/null\", O_WRONLY, 0);\n+\t\tif (wr_fd \u003c 0) {\n+\t\t\tsnprintf(reason, sizeof(reason),\n+\t\t\t\t \"open /dev/null: %s\", strerror(errno));\n+\t\t\tok = 0;\n+\t\t}\n+\t}\n+\n+\t/* Write to /dev/null */\n+\tif (ok) {\n+\t\terrno = 0;\n+\t\tlong n = syscall(__NR_write, (int)wr_fd, \"test\", 4);\n+\n+\t\tif (n != 4) {\n+\t\t\tsnprintf(reason, sizeof(reason), \"write: %s\",\n+\t\t\t\t n \u003c 0 ? strerror(errno) : \"short write\");\n+\t\t\tok = 0;\n+\t\t}\n+\t}\n+\n+\t/* Close both fds */\n+\tif (rd_fd \u003e= 0) {\n+\t\terrno = 0;\n+\t\tif (syscall(__NR_close, (int)rd_fd) != 0 \u0026\u0026 ok) {\n+\t\t\tsnprintf(reason, sizeof(reason), \"close read fd: %s\",\n+\t\t\t\t strerror(errno));\n+\t\t\tok = 0;\n+\t\t}\n+\t}\n+\n+\tif (wr_fd \u003e= 0) {\n+\t\terrno = 0;\n+\t\tif (syscall(__NR_close, (int)wr_fd) != 0 \u0026\u0026 ok) {\n+\t\t\tsnprintf(reason, sizeof(reason), \"close write fd: %s\",\n+\t\t\t\t strerror(errno));\n+\t\t\tok = 0;\n+\t\t}\n+\t}\n+\n+\tif (ok)\n+\t\ttap_ok(\"normal syscall path (open/read/write/close) works\");\n+\telse\n+\t\ttap_fail(\"normal syscall path (open/read/write/close) works\",\n+\t\t\t reason);\n+}\n+\n+/*\n+ * Test 29: read with a NULL buffer and count=0 should return 0\n+ * No access is performed, so KAPI must not reject the NULL pointer.\n+ */\n+static void test_read_null_zero_count(void)\n+{\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_RDONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"read with NULL buffer and count=0 returns 0\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_read, (int)fd, NULL, 0);\n+\n+\tif (ret == 0) {\n+\t\ttap_ok(\"read with NULL buffer and count=0 returns 0\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected 0, got %ld (errno=%s)\",\n+\t\t\t ret, strerror(errno));\n+\t\ttap_fail(\"read with NULL buffer and count=0 returns 0\", msg);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+}\n+\n+/*\n+ * Test 30: write with a NULL buffer and count=0 should return 0\n+ * No access is performed, so KAPI must not reject the NULL pointer.\n+ */\n+static void test_write_null_zero_count(void)\n+{\n+\tlong fd;\n+\n+\terrno = 0;\n+\tfd = kapi_sys_open(\"/dev/null\", O_WRONLY, 0);\n+\tif (fd \u003c 0) {\n+\t\ttap_fail(\"write with NULL buffer and count=0 returns 0\",\n+\t\t\t \"cannot open /dev/null\");\n+\t\treturn;\n+\t}\n+\n+\terrno = 0;\n+\tlong ret = syscall(__NR_write, (int)fd, NULL, 0);\n+\n+\tif (ret == 0) {\n+\t\ttap_ok(\"write with NULL buffer and count=0 returns 0\");\n+\t} else {\n+\t\tchar msg[64];\n+\n+\t\tsnprintf(msg, sizeof(msg), \"expected 0, got %ld (errno=%s)\",\n+\t\t\t ret, strerror(errno));\n+\t\ttap_fail(\"write with NULL buffer and count=0 returns 0\", msg);\n+\t}\n+\n+\tsyscall(__NR_close, (int)fd);\n+}\n+\n+/*\n+ * Test 31: verify dmesg contains KAPI warnings for the invalid tests\n+ */\n+static void test_dmesg_warnings(void)\n+{\n+\tint kmsg_fd;\n+\n+\tif (!kapi_runtime_checks_active()) {\n+\t\ttap_skip(\"dmesg contains expected KAPI warnings\",\n+\t\t\t \"CONFIG_KAPI_RUNTIME_CHECKS not enabled\");\n+\t\treturn;\n+\t}\n+\n+\tkmsg_fd = open(\"/dev/kmsg\", O_RDONLY | O_NONBLOCK);\n+\tif (kmsg_fd \u003c 0) {\n+\t\ttap_skip(\"dmesg contains expected KAPI warnings\",\n+\t\t\t \"cannot open /dev/kmsg\");\n+\t\treturn;\n+\t}\n+\n+\tchar line[4096];\n+\tint found_invalid_bits = 0;\n+\tint found_null = 0;\n+\tssize_t n;\n+\n+\tfor (;;) {\n+\t\tn = read(kmsg_fd, line, sizeof(line) - 1);\n+\t\tif (n \u003e 0) {\n+\t\t\tline[n] = '\\0';\n+\t\t\tif (strstr(line, \"contains invalid bits\"))\n+\t\t\t\tfound_invalid_bits++;\n+\t\t\tif (strstr(line, \"NULL\") \u0026\u0026 strstr(line, \"not allowed\"))\n+\t\t\t\tfound_null++;\n+\t\t} else if (n == -1 \u0026\u0026 errno == EPIPE) {\n+\t\t\t/* Ring buffer wrapped, continue reading */\n+\t\t\tcontinue;\n+\t\t} else {\n+\t\t\t/* EAGAIN (no more messages) or other error */\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tclose(kmsg_fd);\n+\n+\tif (found_invalid_bits \u003e= 2 \u0026\u0026 found_null \u003e= 1) {\n+\t\ttap_ok(\"dmesg contains expected KAPI warnings\");\n+\t} else {\n+\t\tchar msg[128];\n+\n+\t\tsnprintf(msg, sizeof(msg),\n+\t\t\t \"expected invalid_bits\u003e=2 null\u003e=1, got invalid_bits=%d null=%d\",\n+\t\t\t found_invalid_bits, found_null);\n+\t\ttap_fail(\"dmesg contains expected KAPI warnings\", msg);\n+\t}\n+}\n+\n+int main(void)\n+{\n+\tksft_print_header();\n+\tksft_set_plan(NUM_TESTS);\n+\n+\t/* Valid operations (1-4) */\n+\tint fd = test_open_valid();\n+\n+\tif (fd \u003e= 0)\n+\t\ttest_read_valid(fd);\n+\telse\n+\t\ttap_fail(\"read from valid fd\", \"no fd from open\");\n+\n+\ttest_write_valid();\n+\n+\tif (fd \u003e= 0)\n+\t\ttest_close_valid(fd);\n+\telse\n+\t\ttap_fail(\"close valid fd\", \"no fd from open\");\n+\n+\t/* KAPI parameter validation (5-8) */\n+\ttest_open_invalid_flags();\n+\ttest_open_high_mode_bits();\n+\ttest_open_null_path();\n+\ttest_open_flag_bit30();\n+\n+\t/* Boundary conditions and error paths (9-20) */\n+\ttest_read_bad_fd();\n+\ttest_read_zero_count();\n+\ttest_write_zero_count();\n+\ttest_open_long_path();\n+\ttest_read_unmapped_buf();\n+\ttest_write_unmapped_buf();\n+\ttest_close_already_closed();\n+\ttest_open_valid_cloexec();\n+\ttest_write_zero_devnull();\n+\ttest_read_writeonly_fd();\n+\ttest_write_readonly_fd();\n+\ttest_close_fd_9999();\n+\n+\t/* Pipe and lifecycle tests (21-27) */\n+\ttest_read_closed_pipe();\n+\ttest_write_closed_pipe();\n+\ttest_open_directory_on_file();\n+\ttest_open_nonexistent();\n+\ttest_close_stdin();\n+\ttest_read_after_close();\n+\ttest_write_large_count();\n+\n+\t/* Integration (28-31) */\n+\ttest_normal_path();\n+\ttest_read_null_zero_count();\n+\ttest_write_null_zero_count();\n+\ttest_dmesg_warnings();\n+\n+\tksft_finished();\n+\treturn 0;\n+}\n"
}

2/1 2026/10/08 12:29 agent patch-evaluator
7m Model:
core

Args:
null

Results:
{
  "EnableConfigs": [
    "KAPI_SPEC",
    "KAPI_RUNTIME_CHECKS",
    "KAPI_SPEC_DEBUGFS",
    "DEBUG_FS"
  ],
  "FocusSymbols": [
    "kapi_validate_syscall_params",
    "kapi_validate_syscall_return",
    "kapi_validate_param_with_context",
    "kapi_validate_return_value",
    "kapi_export_json",
    "kapi_register_spec",
    "kapi_unregister_spec",
    "kapi_spec_show",
    "kapi_spec_json_show",
    "kapi_list_show"
  ],
  "Reasoning": "The patch series introduces the Kernel API Specification Framework (KAPI). It modifies x86 and generic syscall wrappers (__SYSCALL_DEFINEx) to intercept syscall execution, validating syscall parameters against annotations before running the handler and validating return values afterward (initially annotating sys_open, sys_close, sys_read, sys_write, and sys_madvise). It also adds a debugfs interface under /sys/kernel/debug/kapi/ to expose human-readable and JSON-serialized specifications. This modifies reachable core syscall dispatch and VFS/mm execution paths, making it worth fuzzing. kapi_get_spec is intentionally omitted from FocusSymbols to avoid diluting coverage filters across all syscalls.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.

================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.

- REACHABILITY IS THE PRIMARY GATE:
  Fuzzing can only discover bugs in code that can actually execute in standard virtualized
  environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
  If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
  regardless of whether it adds assertions or complex logic.

- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
  Patch authors routinely label changes as "cleanups", "refactorings", or state
  "No functional change intended". Do NOT take these claims at face value.
  Code refactorings that rearrange logic, introduce helper functions, or alter state management
  in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
  If reachable executable code is modified or refactored, it MUST be fuzzed.

- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
  When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
  BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
  Even if the author believes the invariant always holds, fuzzing is essential to verify whether
  an unusual sequence of operations can violate it.

================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:

- Non-kernel and non-executable changes:
  * Modifications to Documentation/, comments, or spelling fixes.
  * User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
    that do not affect the compiled kernel image (vmlinux) or kernel modules.
  * Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
    that do not alter control flow or data structures.
  * Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
  * Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
    ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
  * Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
    physical PCI hot-unplug or manual sysfs driver unbinding.
  * Code paths exclusive to architectures other than the target architecture.

================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
  * Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
    BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
  * Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
  * Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
  * Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).

================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:

- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
  Do NOT list generic, ubiquitous functions called by almost every program in the corpus
  (including, but not limited to: general memory allocators and deallocators, page fault
  and trap handlers, or core synchronization primitives; this is not an exhaustive list).
  Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
  which severely dilutes fuzzing effort away from the actual changes.

- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
  List functions that specifically implement the logic being added or altered, or direct API entrypoints
  for the subsystem feature under review.

- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
  Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
  distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
  If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
  functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).

================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit 866c60e83179de8f813261f2eb9fcbe27010e491
Author: syz-cluster <triage@syzkaller.com>
Date:   Thu Oct 8 12:29:08 2026 +0000

    syz-cluster: applied patch under review

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/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..818c9b966a0c0
--- /dev/null
+++ b/Documentation/dev-tools/kernel-api-spec.rst
@@ -0,0 +1,648 @@
+.. 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. See
+   `The kapi Tool`_.
+
+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:``).
+* ``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``,
+  ``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
+3. Optionally enable ``CONFIG_KAPI_SPEC_DEBUGFS`` for debugfs interface
+
+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()``.
+
+.. 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
+-----------------
+
+``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,
+
+Tracepoints
+-----------
+
+When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, the syscall validation path emits
+two ftrace tracepoints (in the ``kapi`` trace system) for every syscall that has a
+specification:
+
+- ``kapi_syscall_enter`` -- fired before parameter validation, recording the spec
+  name, the raw syscall argument values, and -- when the spec provides parameter
+  metadata -- a rendered ``name=value`` list: signed integer and file descriptor
+  values are shown in decimal, all other values in hex, and an unnamed parameter
+  as ``arg``.
+- ``kapi_syscall_exit`` -- fired after the handler returns, or in place of the
+  handler when parameter validation rejects the call (the handler is skipped and
+  ``-EINVAL`` is returned). Records the spec name, the return value, and
+  ``spec_match``: 0 when the parameters were rejected or a successful ``fd`` return
+  was not a valid file descriptor, and 1 otherwise. Error codes missing from the
+  spec do not clear it.
+
+Unlike the ``pr_warn_ratelimited`` violation reports, the tracepoints capture every
+spec'd call rather than only violations, and can be filtered with the usual ftrace
+facilities. They require ``CONFIG_TRACEPOINTS`` and stay dormant
+until enabled::
+
+    # echo 1 > /sys/kernel/tracing/events/kapi/enable
+    # cat /sys/kernel/tracing/trace
+     ...  kapi_syscall_enter: sys_read(fd=3, buf=0x7ffd46780b58, count=0x340)
+     ...  kapi_syscall_exit: sys_read = 832 spec_match=1
+     ...  kapi_syscall_enter: sys_open(filename=0x480300, flags=268435456, mode=0x0)
+     ...  kapi_syscall_exit: sys_open = -22 spec_match=0
+
+DebugFS Interface
+=================
+
+The debugfs interface provides runtime access to API specifications:
+
+Directory Structure
+-------------------
+
+::
+
+    /sys/kernel/debug/kapi/
+    ├── list                     # Overview of all registered API specs
+    ├── specs/                   # Per-API specification files
+    │   ├── sys_open             # Human-readable spec for sys_open
+    │   ├── sys_close            # Human-readable spec for sys_close
+    │   ├── sys_read             # Human-readable spec for sys_read
+    │   ├── sys_write            # Human-readable spec for sys_write
+    │   └── sys_madvise          # Human-readable spec for sys_madvise
+    └── specs-json/              # Machine-readable counterpart of specs/
+        ├── sys_open             # JSON spec for sys_open
+        └── ...
+
+Usage Examples
+--------------
+
+List all available API specifications::
+
+    $ cat /sys/kernel/debug/kapi/list
+    Available Kernel API Specifications
+    ===================================
+
+    sys_open - Open or create a file
+    sys_close - Close a file descriptor
+    sys_read - Read data from a file descriptor
+    sys_write - Write data to a file descriptor
+    sys_madvise - Give advice about use of memory
+
+    Total: 5 specifications
+
+Query specific API::
+
+    $ cat /sys/kernel/debug/kapi/specs/sys_open
+    Kernel API Specification
+    ========================
+
+    Name: sys_open
+    Version: 1
+    Description: Open or create a file
+    ...
+
+The ``specs-json/`` files carry the complete specification: for each
+parameter the type class, flags and the constraint (type, range, valid
+mask, enumerated values, alignment, size and the index of the parameter
+holding a buffer's size), the return check, errors, locks, signals,
+signal masks, side effects, state transitions, capabilities, additional
+constraints and structure specifications. Masks and flag words are
+hex strings, enumerations are lower-case tokens such as
+``"constraint_type": "buffer"``, and ``size_param_idx`` is a 0-based
+index into ``parameters`` (``null`` when unused). The ``kapi`` tool
+reads these files with ``--debugfs`` and reports the same data as
+``--vmlinux``.
+
+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)
+
+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. Verify debugfs output is correct
+5. Run scripts/checkpatch.pl on your changes
diff --git a/MAINTAINERS b/MAINTAINERS
index 65e8a4b5c90b1..20020705921a6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14118,6 +14118,19 @@ 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:	include/trace/events/kapi.h
+F:	kernel/api/
+F:	tools/kapi/
+F:	tools/lib/python/kdoc/kdoc_apispec.py
+F:	tools/testing/selftests/kapi/
+
 KERNEL AUTOMOUNTER
 M:	Ian Kent <raven@themaw.net>
 L:	autofs@vger.kernel.org
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/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/fs/open.c b/fs/open.c
index 6b1c14e684a93..9f08c438f162a 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -1424,6 +1424,330 @@ int do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
 }
 
 
+/**
+ * sys_open - Open or create a file
+ * @filename: Pathname of the file to open or create
+ * @flags: File access mode and behavior flags (O_RDONLY, O_WRONLY, O_RDWR, etc.)
+ * @mode: File permission bits for newly created files (only with O_CREAT/O_TMPFILE)
+ *
+ * long-desc: Opens the file named by filename, relative to the current
+ *   working directory if the path is relative. With O_CREAT, the file is
+ *   created if it does not exist. With O_TMPFILE, filename must name an
+ *   existing directory, in which an unnamed file is created. A new file gets
+ *   mode & ~umask as its permission bits.
+ *
+ *   The low two bits of flags (O_ACCMODE) select the access mode: O_RDONLY,
+ *   O_WRONLY or O_RDWR. File creation and file status flags are ORed in.
+ *
+ *   File creation flags: O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC, O_DIRECTORY,
+ *   O_NOFOLLOW, O_CLOEXEC, O_TMPFILE, O_EMPTYPATH. O_EMPTYPATH permits an
+ *   empty filename, which for open() refers to the current working directory.
+ *
+ *   File status flags: O_APPEND, FASYNC, O_DIRECT, O_DSYNC, O_LARGEFILE,
+ *   O_NOATIME, O_NONBLOCK (O_NDELAY), O_PATH, O_SYNC. These become part of the
+ *   file's open file description and can be retrieved with fcntl(F_GETFL). Only
+ *   O_APPEND, O_NONBLOCK, O_DIRECT, O_NOATIME and FASYNC can be changed later
+ *   with fcntl(F_SETFL).
+ *
+ *   On success the lowest-numbered file descriptor not currently open in the
+ *   process is returned.
+ *
+ *   On 64-bit systems, O_LARGEFILE is automatically added to the flags. On 32-bit
+ *   systems, files larger than 2GB require O_LARGEFILE to be explicitly set.
+ *
+ *   open() is equivalent to openat(AT_FDCWD, filename, flags, mode).
+ *
+ * contexts: process, sleepable
+ *
+ * param: filename
+ *   type: path, input
+ *   constraint-type: user_path
+ *   cdesc: Must be a valid null-terminated path string in user memory.
+ *     Maximum path length is PATH_MAX (4096 bytes) including null terminator.
+ *     For relative paths, resolution starts from current working directory.
+ *     The path is followed (symlinks resolved) unless O_NOFOLLOW is specified.
+ *
+ * param: flags
+ *   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_NDELAY | O_DSYNC | O_SYNC |
+ *                         FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW |
+ *                         O_NOATIME | O_CLOEXEC | O_PATH | O_TMPFILE | O_EMPTYPATH)
+ *   cdesc: Should be one of O_RDONLY (0), O_WRONLY (1), or O_RDWR (2) as the
+ *     access mode. Additional flags may be ORed. O_CREAT combined with
+ *     O_DIRECTORY or O_TMPFILE, O_TMPFILE without O_DIRECTORY, and O_TMPFILE
+ *     with read-only mode return EINVAL. With O_PATH, open() silently drops
+ *     every other flag except O_DIRECTORY, O_NOFOLLOW, O_CLOEXEC and
+ *     O_EMPTYPATH (only openat2() rejects them with EINVAL). Unknown flags are
+ *     silently ignored for backward compatibility (unlike openat2 which
+ *     rejects them).
+ *
+ * param: mode
+ *   type: uint, input
+ *   cdesc: Only meaningful when O_CREAT or O_TMPFILE is specified in
+ *     flags. Specifies the file mode bits (permissions and setuid/setgid/sticky
+ *     bits) for a newly created file. The effective mode is (mode & ~umask).
+ *     When O_CREAT/O_TMPFILE is not set, mode is ignored. Mode values exceeding
+ *     S_IALLUGO (07777) are masked off.
+ *
+ * return:
+ *   type: int
+ *   check-type: fd
+ *   success: >= 0
+ *   desc: On success, returns a new file descriptor (non-negative integer).
+ *     The returned file descriptor is the lowest-numbered descriptor not
+ *     currently open for the process. On error, returns a negative error code.
+ *
+ * error: EACCES, Permission denied
+ *   desc: The requested access to the file is not allowed, or search permission
+ *     is denied for one of the directories in the path prefix of pathname, or
+ *     the file did not exist yet and write access to the parent directory is
+ *     not allowed, or O_TRUNC is specified but write permission is denied, or
+ *     pathname is a device special file on a nodev mount, or O_CREAT on an
+ *     existing FIFO or regular file in a sticky directory is refused by
+ *     protected_fifos or protected_regular, or a security module denies the
+ *     open.
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ *   desc: O_NONBLOCK was specified and a conflicting lease is held on the file,
+ *     so the open would have to wait for the lease to break. break_lease()
+ *     returns -EWOULDBLOCK, which has the same value as EAGAIN.
+ *
+ * error: EBUSY, Device or resource busy
+ *   desc: O_EXCL was specified in flags and pathname refers to a block device
+ *     that is in use by the system (e.g., it is mounted).
+ *
+ * error: EDQUOT, Disk quota exceeded
+ *   desc: O_CREAT is specified and the file does not exist, and the user's quota
+ *     of disk blocks or inodes on the filesystem has been exhausted.
+ *
+ * error: EEXIST, File exists
+ *   desc: O_CREAT and O_EXCL were specified in flags, but pathname already exists.
+ *     This error is atomic with respect to file creation - it prevents race
+ *     conditions (TOCTOU) when creating files.
+ *
+ * error: EFAULT, Bad address
+ *   desc: pathname points outside the process's accessible address space.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: A signal arrived while the open was blocked waiting for the partner
+ *     of a FIFO open (fifo_open), waiting for a conflicting lease to break
+ *     (__break_lease), or inside a driver's open method. The kernel-internal
+ *     -ERESTARTSYS is reported as EINTR unless the handler uses SA_RESTART.
+ *
+ * error: EINVAL, Invalid argument
+ *   desc: Returned for several conditions: (1) Invalid O_* flag combinations
+ *     (O_CREAT with O_DIRECTORY, O_CREAT with O_TMPFILE, O_TMPFILE without
+ *     O_DIRECTORY, O_TMPFILE with read-only access). (2) O_DIRECT requested
+ *     but the filesystem does not support it.
+ *
+ * error: EISDIR, Is a directory
+ *   desc: pathname refers to a directory and the access requested involved
+ *     writing (O_WRONLY, O_RDWR, or O_TRUNC). Also returned when O_CREAT is
+ *     specified and pathname names an existing directory or ends in a slash.
+ *
+ * error: ELOOP, Too many symbolic links
+ *   desc: Too many symbolic links were encountered in resolving pathname, or
+ *     O_NOFOLLOW was specified but pathname refers to a symbolic link. With
+ *     O_PATH and O_NOFOLLOW the symbolic link itself is opened instead.
+ *
+ * error: EMFILE, Too many open files
+ *   desc: The per-process limit on the number of open file descriptors has been
+ *     reached. This limit is RLIMIT_NOFILE (default typically 1024, max set by
+ *     /proc/sys/fs/nr_open).
+ *
+ * error: ENAMETOOLONG, File name too long
+ *   desc: pathname was too long, exceeding PATH_MAX (4096) bytes, or a single
+ *     path component exceeded NAME_MAX (usually 255) bytes.
+ *
+ * error: ENFILE, Too many open files in system
+ *   desc: The system-wide limit on the total number of open files has been
+ *     reached (/proc/sys/fs/file-max). Processes with CAP_SYS_ADMIN can exceed
+ *     this limit.
+ *
+ * error: ENODEV, No such device
+ *   desc: The filesystem or driver open method failed with ENODEV, or the
+ *     file's inode has no file operations assigned. A device special file with
+ *     no registered device fails with ENXIO instead.
+ *
+ * error: ENOENT, No such file or directory
+ *   desc: A directory component in pathname does not exist or is a dangling
+ *     symbolic link, or O_CREAT is not set and the named file does not exist,
+ *     or pathname is an empty string and O_EMPTYPATH is not specified.
+ *
+ * error: ENOMEM, Out of memory
+ *   desc: The kernel could not allocate sufficient memory for the file structure,
+ *     path lookup structures, or the filename buffer.
+ *
+ * error: ENOSPC, No space left on device
+ *   desc: O_CREAT was specified and the file does not exist, and the directory
+ *     or filesystem containing the file has no room for a new file entry.
+ *
+ * error: ENOTDIR, Not a directory
+ *   desc: A component used as a directory in pathname is not actually a directory,
+ *     or O_DIRECTORY was specified and pathname was not a directory.
+ *
+ * error: ENXIO, No such device or address
+ *   desc: O_NONBLOCK | O_WRONLY is set and the named file is a FIFO and no
+ *     process has the FIFO open for reading. Also returned when opening a device
+ *     special file whose device does not exist (chrdev_open, blkdev_open), or
+ *     when opening a socket inode.
+ *
+ * error: EOPNOTSUPP, Operation not supported
+ *   desc: The filesystem containing pathname does not support O_TMPFILE.
+ *
+ * error: EOVERFLOW, Value too large for defined data type
+ *   desc: pathname refers to a regular file that is too large to be opened.
+ *     This occurs on 32-bit systems without O_LARGEFILE when the file size
+ *     exceeds 2GB (2^31 - 1 bytes).
+ *
+ * error: EPERM, Operation not permitted
+ *   desc: O_NOATIME flag was specified but the effective UID of the caller did
+ *     not match the owner of the file and the caller is not privileged, or the
+ *     file is append-only and O_TRUNC was specified or write mode without
+ *     O_APPEND, or the file is immutable, or a seal prevents the operation.
+ *
+ * error: EROFS, Read-only file system
+ *   desc: pathname refers to a file on a read-only filesystem and write access
+ *     was requested.
+ *
+ * error: ETXTBSY, Text file busy
+ *   desc: Write access or O_TRUNC was requested for an executable image that
+ *     is currently being executed, or O_TRUNC was requested on an active swap
+ *     file. A swap file can otherwise be opened for writing.
+ *
+ * lock: files->file_lock
+ *   type: spinlock
+ *   acquired: true
+ *   released: true
+ *   desc: Acquired when allocating a file descriptor slot. Held briefly during
+ *     fd allocation via alloc_fd() and released before the syscall returns.
+ *
+ * lock: inode->i_rwsem (parent directory)
+ *   type: semaphore
+ *   acquired: true
+ *   released: true
+ *   desc: Conditional, taken only when the final component is not resolved by
+ *     the lockless dcache lookup. lookup_open() takes it exclusively with
+ *     inode_lock() when O_CREAT is set and shared with inode_lock_shared()
+ *     otherwise. Slow-path lookup of path components takes it shared. Released
+ *     when the lookup returns. The open path has no killable variant.
+ *
+ * lock: RCU read-side
+ *   type: rcu
+ *   acquired: true
+ *   released: true
+ *   desc: Path lookup uses RCU mode initially for performance. If RCU lookup
+ *     fails (returns -ECHILD), falls back to reference-based lookup.
+ *
+ * signal: Any signal
+ *   direction: receive
+ *   action: return
+ *   condition: When blocked in an interruptible wait
+ *   desc: The syscall may be interrupted while waiting for the partner of a
+ *     FIFO open (fifo_open), for a conflicting lease to break (__break_lease),
+ *     or inside a driver's open method. The wait returns -ERESTARTSYS, which
+ *     is restarted after the handler with SA_RESTART and reported as EINTR
+ *     otherwise.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: yes
+ *
+ * side-effect: resource_create | alloc_memory
+ *   target: file descriptor, file structure, dentry cache
+ *   desc: Allocates a new file descriptor in the process's fd table. Allocates
+ *     a struct file from the filp slab cache. May allocate dentries and inodes
+ *     during path lookup. System-wide file count (nr_files) is incremented.
+ *   reversible: yes
+ *
+ * side-effect: filesystem
+ *   target: filesystem, inode
+ *   condition: When O_CREAT is specified and file doesn't exist
+ *   desc: Creates a new file on the filesystem. Creates new inode, allocates
+ *     data blocks as needed, and creates directory entry. Updates parent
+ *     directory mtime and ctime.
+ *   reversible: no
+ *
+ * side-effect: filesystem
+ *   target: file content
+ *   condition: When O_TRUNC is specified for existing file
+ *   desc: Truncates the file to zero length, releasing data blocks. Updates
+ *     file mtime and ctime. May trigger notifications to lease holders.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: inode timestamps
+ *   condition: When a symlink is followed, or O_TRUNC or O_CREAT takes effect
+ *   desc: Opening does not update the atime of the opened file. Reads update it
+ *     later, and O_NOATIME only affects those reads. Following a symlink in
+ *     the path may update the symlink's atime, subject to the mount atime
+ *     options. O_TRUNC and file creation update mtime and ctime.
+ *
+ * capability: CAP_DAC_OVERRIDE
+ *   type: bypass_check
+ *   allows: Bypass file read, write, and execute permission checks
+ *   without: Standard DAC (discretionary access control) checks are applied
+ *   condition: Checked when file permission would otherwise deny access
+ *
+ * capability: CAP_DAC_READ_SEARCH
+ *   type: bypass_check
+ *   allows: Bypass read permission on files and search permission on directories
+ *   without: Must have read permission on file or search permission on directory
+ *   condition: Checked during path traversal and file open
+ *
+ * capability: CAP_FOWNER
+ *   type: bypass_check
+ *   allows: Use O_NOATIME on files not owned by caller
+ *   without: O_NOATIME returns EPERM if caller is not file owner
+ *   condition: Checked when O_NOATIME is specified and caller is not owner
+ *
+ * capability: CAP_SYS_ADMIN
+ *   type: increase_limit
+ *   allows: Exceed the system-wide file limit (file-max)
+ *   without: Returns ENFILE when system limit is reached
+ *   condition: Checked in alloc_empty_file() when nr_files >= max_files
+ *
+ * constraint: RLIMIT_NOFILE (per-process fd limit)
+ *   desc: The returned file descriptor must be less than the process's
+ *     RLIMIT_NOFILE limit. Default is typically 1024, maximum is controlled
+ *     by /proc/sys/fs/nr_open (default 1048576). Exceeding returns EMFILE.
+ *   expr: fd < rlimit(RLIMIT_NOFILE)
+ *
+ * constraint: file-max (system-wide limit)
+ *   desc: System-wide limit on open files in /proc/sys/fs/file-max. Processes
+ *     without CAP_SYS_ADMIN receive ENFILE when this limit is reached. The
+ *     limit is computed based on system memory at boot time.
+ *   expr: nr_files < files_stat.max_files || capable(CAP_SYS_ADMIN)
+ *
+ * constraint: PATH_MAX
+ *   desc: Maximum length of pathname including null terminator is PATH_MAX
+ *     (4096 bytes). Individual path components must not exceed NAME_MAX (255).
+ *
+ * examples: fd = open("/etc/passwd", O_RDONLY);  // Read existing file
+ *   fd = open("/tmp/newfile", O_WRONLY | O_CREAT | O_TRUNC, 0644);  // Create/truncate
+ *   fd = open("/tmp/lockfile", O_WRONLY | O_CREAT | O_EXCL, 0600);  // Exclusive create
+ *   fd = open("/dev/null", O_RDWR);  // Open device
+ *   fd = open("/tmp", O_RDONLY | O_DIRECTORY);  // Open directory
+ *   fd = open("/tmp", O_TMPFILE | O_RDWR, 0600);  // Anonymous temp file
+ *
+ * notes: O_RDONLY is defined as 0, so (flags & O_RDONLY) always evaluates to zero.
+ *   Test access mode using (flags & O_ACCMODE) == O_RDONLY.
+ *
+ *   When O_CREAT is specified without O_EXCL, there is a race condition between
+ *   testing for file existence and creating it. Use O_CREAT | O_EXCL for atomic
+ *   exclusive file creation.
+ *
+ *   O_CLOEXEC should be used in multithreaded programs to prevent file descriptor
+ *   leaks to child processes between fork() and execve().
+ *
+ *   O_DIRECT has alignment requirements that vary by filesystem. Use statx()
+ *   with STATX_DIOALIGN (Linux 6.1+) to query requirements. Unaligned I/O may
+ *   fail with EINVAL or fall back to buffered I/O.
+ *
+ *   O_PATH opens a file descriptor that can be used only for certain operations
+ *   (fstat, dup, fcntl, close, fchdir on directories, as dirfd for *at() calls).
+ *   I/O operations will fail with EBADF.
+ */
 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
 {
 	if (force_o_largefile())
@@ -1537,6 +1861,239 @@ int filp_close(struct file *filp, fl_owner_t id)
 }
 EXPORT_SYMBOL(filp_close);
 
+/**
+ * sys_close - Close a file descriptor
+ * @fd: The file descriptor to close
+ *
+ * long-desc: Terminates access to an open file descriptor, releasing the file
+ *   descriptor for reuse by subsequent open(), dup(), or similar syscalls.
+ *
+ *   Traditional POSIX advisory record locks held by the process on the
+ *   associated file are released when any of its fds for that inode is
+ *   closed, not only the last one. OFD locks and flock locks are associated
+ *   with the open file description and are only released when the last
+ *   reference to that open file description is dropped.
+ *
+ *   Closing a file descriptor drops one reference to its open file
+ *   description. Other descriptors (dup(), fork(), SCM_RIGHTS messages in
+ *   flight) and operations still running on the file, such as a concurrent
+ *   read(), also hold references. Only when the last reference is dropped are
+ *   the associated resources freed. If the file was previously unlinked, the
+ *   file itself is deleted when the last reference is dropped.
+ *
+ *   Except when close() fails with EBADF, the file descriptor is released even
+ *   when close() returns an error, because it is released before the flush
+ *   that may fail. POSIX leaves the state of the descriptor unspecified after
+ *   EINTR. Retrying close() after an error may close an unrelated file
+ *   descriptor that another thread has since been given.
+ *
+ *   Errors returned from close() come only from the file's ->flush() method,
+ *   called by filp_flush(). Errors from ->release() are not reported.
+ *   Filesystems without a ->flush() method, such as ext4, xfs and btrfs, never
+ *   report write errors from close(). Network filesystems such as NFS, CIFS and
+ *   FUSE implement ->flush() and report deferred write errors (EIO, ENOSPC,
+ *   EDQUOT) at close time. A successful return does not mean the data reached
+ *   storage; call fsync() before close() for that.
+ *
+ *   On close, the following cleanup operations are performed: the ->flush()
+ *   method is called if the file has one, POSIX advisory locks are removed,
+ *   dnotify registrations are cleaned up, and the file reference is released.
+ *   If this was the last reference, additional cleanup includes: fsnotify close
+ *   notification, epoll cleanup, OFD, flock and lease removal, FASYNC cleanup,
+ *   the ->release() method, and the file structure deallocation.
+ *
+ * contexts: process, sleepable
+ *
+ * param: fd
+ *   type: fd, input
+ *   constraint-type: range(0, INT_MAX)
+ *   cdesc: Must be a valid, open file descriptor for the current process.
+ *     The value 0, 1, or 2 (stdin, stdout, stderr) may be closed like any other
+ *     fd, though this is unusual and may cause issues with libraries that assume
+ *     these descriptors are valid. The parameter is unsigned int to match kernel
+ *     file descriptor table indexing. A value that is not open, including any
+ *     value at or above the current table size, fails with EBADF.
+ *
+ * return:
+ *   type: int
+ *   check-type: exact
+ *   success: 0
+ *   desc: Returns 0 on success. On error, returns a negative error code. Except
+ *     for EBADF, the file descriptor is still closed when an error is returned
+ *     and must not be used again. The error comes from the file's ->flush()
+ *     method, not from the fd remaining open. With EBADF, nothing was released.
+ *
+ * error: EBADF, Bad file descriptor
+ *   desc: fd is at or above the file descriptor table size, has no file
+ *     assigned (including a slot reserved by a concurrent open() that has not
+ *     installed its file yet), or was already closed. This is the only error
+ *     for which no file descriptor was released.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: The flush operation was interrupted by a signal before completion.
+ *     This occurs when a driver's ->flush() method (for example wdm_flush() in
+ *     drivers/usb/class/cdc-wdm.c) performs an interruptible wait that receives
+ *     a signal. The file descriptor is still released and must not be used
+ *     again. Kernel-internal restart codes (ERESTARTSYS,
+ *     ERESTARTNOINTR, ERESTARTNOHAND, ERESTART_RESTARTBLOCK) are converted to
+ *     EINTR because restarting the syscall would be incorrect once the fd is
+ *     freed.
+ *
+ * error: EIO, I/O error
+ *   desc: The file's ->flush() method reported an I/O error, typically a
+ *     deferred write error on a network filesystem such as NFS, CIFS or FUSE,
+ *     or an error from a driver's ->flush(). Previously buffered write data
+ *     may have been lost.
+ *
+ * error: ENOSPC, No space left on device
+ *   desc: The file's ->flush() method reported that there was insufficient
+ *     space to flush buffered writes, for example on NFS when the server runs
+ *     out of space between write() and close().
+ *
+ * error: EDQUOT, Disk quota exceeded
+ *   desc: The file's ->flush() method reported that the user's disk quota was
+ *     exceeded while flushing buffered writes, for example on NFS when the
+ *     quota is exceeded between write() and close().
+ *
+ * lock: files->file_lock
+ *   type: spinlock
+ *   acquired: true
+ *   released: true
+ *   desc: Taken by file_close_fd() to look up and clear the fd slot atomically,
+ *     so two concurrent close() calls on one fd cannot both obtain the struct
+ *     file. Dropped before ->flush() and the final fput. From then on the fd
+ *     number may be handed out again. An fd reserved by a concurrent open() but
+ *     not yet installed has a NULL slot, so close() returns EBADF.
+ *
+ * lock: file->f_lock
+ *   type: spinlock
+ *   acquired: true
+ *   released: true
+ *   desc: Taken from __fput() on the last reference, by eventpoll_release_file()
+ *     when the file is registered with epoll, and by fasync_remove_entry()
+ *     (through the ->fasync() method) when FASYNC is set. Protects the epoll
+ *     and fasync links of the file.
+ *
+ * lock: ep->mtx
+ *   type: mutex
+ *   acquired: true
+ *   released: true
+ *   desc: Acquired during epoll cleanup if the file was monitored by epoll.
+ *     Used to safely remove the file from epoll interest lists.
+ *
+ * lock: flc_lock
+ *   type: spinlock
+ *   acquired: true
+ *   released: true
+ *   desc: File lock context spinlock. Taken by locks_remove_posix() from
+ *     filp_flush() on every close when the file has POSIX locks, and by
+ *     locks_remove_file() from __fput() on the last reference to remove OFD,
+ *     flock, and lease locks.
+ *
+ * signal: pending_signals
+ *   direction: receive
+ *   action: return
+ *   condition: When close-time flush performs interruptible wait
+ *   desc: If the close-time ->flush() method (for example wdm_flush() in
+ *     cdc-wdm) performs an interruptible wait and a signal is pending, the wait
+ *     is interrupted. Any kernel restart codes are converted to EINTR since
+ *     close cannot be restarted after the fd is freed.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: no
+ *
+ * side-effect: resource_destroy | irreversible
+ *   target: File descriptor table entry
+ *   desc: The file descriptor is removed from the process's file descriptor
+ *     table, making the fd number available for reuse by subsequent open(),
+ *     dup(), or similar calls. This happens before the flush that may fail, so
+ *     an error return does not undo it.
+ *   condition: Always (when fd is valid)
+ *   reversible: no
+ *
+ * side-effect: lock_release
+ *   target: POSIX advisory locks, OFD locks, flock locks
+ *   desc: POSIX locks held by this process on the inode are removed on every
+ *     close via locks_remove_posix() in filp_flush(), except for O_PATH files.
+ *     OFD and flock locks are removed via locks_remove_file() in __fput() only
+ *     when this is the last reference to the open file description.
+ *   condition: Any close of a non-O_PATH file for POSIX locks, last reference
+ *     for OFD and flock locks
+ *   reversible: no
+ *
+ * side-effect: resource_destroy
+ *   target: File leases
+ *   desc: Any file leases held on the file are removed during locks_remove_file()
+ *     when this is the last reference to the open file description.
+ *   condition: File had leases and this is the last reference
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: dnotify registrations
+ *   desc: Directory notification (dnotify) registrations associated with this
+ *     file are cleaned up via dnotify_flush(). This only applies to directories.
+ *   condition: File is a directory with dnotify registrations
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: epoll interest lists
+ *   desc: If the file was being monitored by epoll instances, it is removed
+ *     from those interest lists via eventpoll_release(), which runs from
+ *     __fput() only on the last reference. While other references remain, the
+ *     epoll registrations stay active.
+ *   condition: File was added to epoll instances and this is the last reference
+ *   reversible: no
+ *
+ * side-effect: filesystem
+ *   target: Buffered data
+ *   desc: The file's ->flush() method runs before the file reference is
+ *     dropped (for example on NFS, CIFS and FUSE) and may return errors such as
+ *     EIO, ENOSPC or EDQUOT.
+ *   condition: The filesystem or driver provides a ->flush() method
+ *   reversible: no
+ *
+ * side-effect: free_memory
+ *   target: struct file and related structures
+ *   desc: When this is the last reference to the file, the file structure is
+ *     freed and the dentry and mount references are released.
+ *   condition: This is the last reference to the file
+ *   reversible: no
+ *
+ * side-effect: filesystem
+ *   target: Unlinked file deletion
+ *   desc: If the file was previously unlinked (deleted) but kept open, closing
+ *     the last reference causes the actual file data to be removed from the
+ *     filesystem and the inode to be freed.
+ *   condition: File was unlinked and this is the last reference
+ *   reversible: no
+ *
+ * state-trans: file_descriptor
+ *   from: open
+ *   to: closed/free
+ *   condition: Valid fd passed to close
+ *   desc: The file descriptor transitions from open (usable) to closed (invalid).
+ *     The fd number becomes available for reuse.
+ *
+ * state-trans: file_reference_count
+ *   from: n
+ *   to: n-1 (or freed if n was 1)
+ *   condition: Always on successful fd lookup
+ *   desc: The file's reference count is decremented. If this was the last
+ *     reference, the file is fully cleaned up and freed.
+ *
+ * examples: close(fd);  // Ignoring the result loses ->flush() errors
+ *   if (close(fd) == -1) perror("close");  // Log errors for debugging
+ *   fsync(fd); close(fd);  // Ensure data persistence before closing
+ *
+ * notes: close() drops its reference with fput_close_sync(), so when it is the last
+ *   reference __fput() runs synchronously in the calling task before close()
+ *   returns, instead of being deferred to task work.
+ *
+ *   Calling close() on a file descriptor while another thread is using it
+ *   (e.g., in a blocking read() or write()) does not interrupt the blocked
+ *   operation. The blocked operation continues on the underlying file and
+ *   may complete even after close() returns.
+ */
 /*
  * Careful here! We test whether the file pointer is NULL before
  * releasing the fd. This ensures that one clone task can't release
diff --git a/fs/read_write.c b/fs/read_write.c
index e8c14e2760b20..a9c07c21f5888 100644
--- a/fs/read_write.c
+++ b/fs/read_write.c
@@ -720,6 +720,303 @@ ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
 	return ret;
 }
 
+/**
+ * sys_read - Read data from a file descriptor
+ * @fd: File descriptor to read from
+ * @buf: User-space buffer to read data into
+ * @count: Maximum number of bytes to read
+ *
+ * long-desc: Reads at most count bytes from fd into the user buffer buf. For
+ *   seekable files (regular files, block devices), the read begins at the
+ *   current file offset, and the file offset is advanced by the number of
+ *   bytes read. For stream files (FMODE_STREAM, such as pipes, FIFOs, and
+ *   sockets), the file offset is not used. Other files, including many
+ *   character devices, keep a file offset although a driver may ignore it.
+ *
+ *   If count is zero, the usual checks still run and the file's read method is
+ *   still called. Common implementations (regular files, pipes, sockets)
+ *   return zero without transferring data.
+ *
+ *   On success the number of bytes read is returned; zero means end of file
+ *   for regular files. A read can return fewer bytes than requested, for
+ *   example near end of file, on a pipe, socket or terminal with less data
+ *   pending, or when a signal arrives after some data was copied.
+ *
+ *   On Linux, read() transfers at most MAX_RW_COUNT (INT_MAX & PAGE_MASK,
+ *   0x7ffff000 with 4KB pages, just under 2GB) bytes per call, regardless of
+ *   whether the filesystem would allow more. This is to avoid issues with
+ *   signed arithmetic overflow on 32-bit systems.
+ *
+ *   POSIX allows reads that are interrupted after reading some data to either
+ *   return -1 (with errno set to EINTR) or return the number of bytes already
+ *   read. Linux follows the latter behavior: if data has been read before a
+ *   signal arrives, the call returns the bytes read rather than failing.
+ *
+ * contexts: process, sleepable
+ *
+ * param: fd
+ *   type: fd, input
+ *   constraint-type: range(0, INT_MAX)
+ *   cdesc: Must be a valid, open file descriptor with read permission.
+ *     The file must have been opened with O_RDONLY or O_RDWR. Special values
+ *     like AT_FDCWD are not valid. File descriptors for directories return
+ *     EISDIR. Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are
+ *     valid if open and readable.
+ *
+ * param: buf
+ *   type: user_ptr, output
+ *   constraint-type: buffer(2)
+ *   cdesc: Must point to a writable user-space region of at least count bytes.
+ *     The range is checked via access_ok() and a range outside user space
+ *     fails with EFAULT. NULL is not rejected by that check. With a count of 0
+ *     the call returns 0 in common cases. With a count above 0 the copy fails
+ *     with EFAULT, or the call returns a short count if some data was copied
+ *     first. The buffer may be partially written if an error occurs mid-read.
+ *     O_DIRECT reads may require block-size alignment (see STATX_DIOALIGN).
+ *
+ * param: count
+ *   type: uint, input
+ *   cdesc: Maximum number of bytes to read. The range buf to buf + count must
+ *     lie within user space or access_ok() fails with EFAULT, which includes
+ *     counts that do not fit in ssize_t on 64-bit kernels. A count of 0 passes
+ *     the same checks and the file's read method is still called, typically
+ *     returning 0 without transferring data. After access_ok() and
+ *     rw_verify_area() succeed, counts above MAX_RW_COUNT (INT_MAX &
+ *     PAGE_MASK, 0x7ffff000 with 4KB pages) are clamped to MAX_RW_COUNT.
+ *
+ * return:
+ *   type: int
+ *   check-type: range
+ *   success: >= 0
+ *   desc: On success, returns the number of bytes read (non-negative). Zero
+ *     indicates end-of-file (EOF) for regular files, or no data available
+ *     from a device that does not block. The return value may be less than
+ *     count if fewer bytes were available (short read). Partial reads are
+ *     not errors. On error, returns a negative error code.
+ *
+ * error: EBADF, Bad file descriptor
+ *   desc: fd is not a valid file descriptor, or fd was not opened for reading.
+ *     This includes file descriptors opened with O_WRONLY, O_PATH, or file
+ *     descriptors that have been closed. Also returned if the file structure
+ *     does not have FMODE_READ set.
+ *
+ * error: EFAULT, Bad address
+ *   desc: buf points outside the accessible address space. The buffer address
+ *     failed access_ok() validation. Can also occur if a fault happens during
+ *     copy_to_user() when transferring data to user space after the read
+ *     completes in kernel space.
+ *
+ * error: EINVAL, Invalid argument
+ *   desc: Returned in several cases: (1) The file has no read or read_iter
+ *     method (FMODE_CAN_READ is not set). (2) The file was opened with
+ *     O_DIRECT and the buffer alignment, offset, or count does not meet the
+ *     filesystem's alignment requirements. (3) For timerfd file descriptors,
+ *     the buffer is smaller than 8 bytes. (4) rw_verify_area() rejects a
+ *     negative file position, or a position plus count that overflows, on a
+ *     file without FOP_UNSIGNED_OFFSET. (5) The count, cast to ssize_t, is
+ *     negative (32-bit kernels only).
+ *
+ * error: EISDIR, Is a directory
+ *   desc: fd refers to a directory. Directories cannot be read using read();
+ *     use getdents64() instead. This error is returned by the generic_read_dir()
+ *     handler installed for directory file operations.
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ *   desc: fd refers to a file (pipe, socket, device) that is marked non-blocking
+ *     (O_NONBLOCK) and the read would block. Equivalent to EWOULDBLOCK. The
+ *     application should retry the read later or use select/poll/epoll.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: The call was interrupted by a signal before any data was read. This
+ *     only occurs if no data has been transferred; if some data was read before
+ *     the signal, the call returns the number of bytes read. The caller should
+ *     typically restart the read.
+ *
+ * error: EIO, Input/output error
+ *   desc: A low-level I/O error occurred. For regular files, this typically
+ *     indicates a hardware error on the storage device, a filesystem error,
+ *     or a network filesystem timeout. For terminals, it is returned when a
+ *     background process group reads its controlling terminal and SIGTTIN is
+ *     ignored or blocked, or the process group is orphaned.
+ *
+ * error: EOVERFLOW, Value too large for defined data type
+ *   desc: Returned by rw_verify_area() only for files with FOP_UNSIGNED_OFFSET
+ *     (for example /proc/pid/mem) when the file position is negative, meaning
+ *     above LLONG_MAX, and count is at least -pos so that the read would wrap
+ *     past the end of the 64-bit offset space. Files without
+ *     FOP_UNSIGNED_OFFSET get EINVAL for a negative position or a position
+ *     plus count that overflows.
+ *
+ * error: ENOBUFS, No buffer space available
+ *   desc: Returned when reading from pipe-based watch queues (CONFIG_WATCH_QUEUE)
+ *     when the buffer is too small to hold a complete notification, or when
+ *     reading packets from pipes with PIPE_BUF_FLAG_WHOLE set.
+ *
+ * error: ERESTARTSYS, Restart system call (internal)
+ *   desc: Internal error code indicating the syscall should be restarted. This
+ *     is typically translated to EINTR if SA_RESTART is not set on the signal
+ *     handler, or the syscall is transparently restarted if SA_RESTART is set.
+ *     User space should not see this error code directly.
+ *
+ * error: EACCES, Permission denied
+ *   desc: The security subsystem (LSM such as SELinux or AppArmor) denied
+ *     the read operation via security_file_permission(). This can occur even
+ *     if the file was successfully opened, as LSM policies may enforce checks
+ *     on each operation.
+ *
+ * error: EPERM, Operation not permitted
+ *   desc: Returned by fanotify permission events (CONFIG_FANOTIFY_ACCESS_PERMISSIONS)
+ *     when a user-space fanotify listener denies the read operation via
+ *     fsnotify_file_area_perm().
+ *
+ * error: ENODATA, No data available
+ *   desc: Returned when a filesystem built on netfs (for example AFS or Ceph)
+ *     ends a read subrequest short without making progress.
+ *
+ * error: EOPNOTSUPP, Operation not supported
+ *   desc: Returned by the read method of the file, not by the generic VFS
+ *     path. For example, a socket whose protocol has no receive method
+ *     (sock_no_recvmsg) fails with EOPNOTSUPP. A file with no read method at
+ *     all fails with EINVAL instead.
+ *
+ * lock: file->f_pos_lock
+ *   type: mutex
+ *   acquired: true
+ *   released: true
+ *   desc: For regular files that require atomic position updates (FMODE_ATOMIC_POS),
+ *     the f_pos_lock mutex is acquired by fdget_pos() at syscall entry and released
+ *     by fdput_pos() at syscall exit. This serializes concurrent reads that share
+ *     the same file description. Not acquired for files opened with FMODE_STREAM
+ *     (pipes, sockets) or when the file is not shared.
+ *
+ * lock: Filesystem-specific locks
+ *   type: custom
+ *   acquired: true
+ *   released: true
+ *   desc: The filesystem's read_iter or read method may acquire additional locks.
+ *     For regular files, this typically includes the inode's i_rwsem for certain
+ *     operations. For pipes, the pipe->mutex is acquired. For sockets, socket
+ *     lock is acquired. These are internal to the file operation and released
+ *     before return.
+ *
+ * lock: RCU read-side
+ *   type: rcu
+ *   acquired: true
+ *   released: true
+ *   desc: Taken only when the files_struct is shared with other threads, in
+ *     which case the fd lookup in fdget() uses RCU and takes a file reference.
+ *     A private table needs no RCU. The RCU read lock is acquired and released
+ *     internally by the fd lookup path, not held across the entire syscall.
+ *     fdput() releases the file reference count, not the RCU lock.
+ *
+ * signal: Any signal
+ *   direction: receive
+ *   action: return
+ *   condition: When blocked waiting for data on interruptible operations
+ *   desc: The syscall may be interrupted by signals while waiting for data to
+ *     become available (pipes, sockets, terminals). If interrupted before any
+ *     data is read, returns -EINTR or -ERESTARTSYS. If data has already been
+ *     read, returns the number of bytes read.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: yes
+ *
+ * side-effect: file_position
+ *   target: file->f_pos
+ *   condition: For seekable files when read succeeds (returns > 0)
+ *   desc: The file offset (f_pos) is advanced by the number of bytes read.
+ *     For stream files (FMODE_STREAM such as pipes and sockets), the offset
+ *     is not used or modified. The offset update is protected by f_pos_lock
+ *     when the file is shared between threads/processes.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: inode access time (atime)
+ *   condition: When the read_iter implementation calls file_accessed() and
+ *     O_NOATIME is not set
+ *   desc: The filesystem's read_iter implementation updates atime via
+ *     file_accessed() and touch_atime(). Buffered reads (filemap_read) do this
+ *     after the data loop, including at EOF and after errors, but not for a
+ *     count of 0. O_DIRECT reads (generic_file_read_iter) do it before the
+ *     I/O. The update may be suppressed by mount options (noatime, relatime),
+ *     the O_NOATIME flag, or if the filesystem does not support atime.
+ *     Relatime updates atime only if it is older than mtime or ctime, or more
+ *     than a day old.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: task I/O accounting
+ *   condition: When CONFIG_TASK_XACCT is enabled and the read method is invoked
+ *   desc: Updates the current task's I/O accounting statistics. The rchar field
+ *     (read characters) is incremented by bytes read via add_rchar() only on
+ *     successful reads (ret > 0). The syscr field (syscall read count) is
+ *     incremented via inc_syscr() once the read method has been invoked, even
+ *     if it fails, but not when the FMODE_READ, FMODE_CAN_READ, access_ok() or
+ *     rw_verify_area() checks fail. These statistics are visible in
+ *     /proc/[pid]/io.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: fsnotify events
+ *   condition: When read returns > 0
+ *   desc: Generates an FS_ACCESS fsnotify event via fsnotify_access() allowing
+ *     inotify, fanotify, and dnotify watchers to be notified of the read. This
+ *     occurs after data transfer completes successfully.
+ *   reversible: no
+ *
+ * constraint: MAX_RW_COUNT
+ *   desc: After the access_ok() and rw_verify_area() checks pass, the count
+ *     parameter is silently clamped to MAX_RW_COUNT (INT_MAX & PAGE_MASK, just
+ *     under 2GB) to prevent integer overflow in internal calculations. This is
+ *     transparent to the caller. The syscall succeeds but reads at most
+ *     MAX_RW_COUNT bytes.
+ *   expr: actual_count = min(count, MAX_RW_COUNT)
+ *
+ * constraint: File must be open for reading
+ *   desc: The file descriptor must have been opened with O_RDONLY or O_RDWR.
+ *     Files opened with O_WRONLY or O_PATH lack FMODE_READ and return EBADF.
+ *     Files that lack FMODE_CAN_READ because they have no read or read_iter
+ *     method return EINVAL.
+ *   expr: (file->f_mode & FMODE_READ) && (file->f_mode & FMODE_CAN_READ)
+ *
+ * examples: n = read(fd, buf, sizeof(buf));  // Basic read
+ *   n = read(STDIN_FILENO, buf, 1024);  // Read from stdin
+ *   while ((n = read(fd, buf, 4096)) > 0) { process(buf, n); }  // Read loop
+ *   if (read(fd, buf, count) == 0) { handle_eof(); }  // Check for EOF
+ *
+ * notes: The behavior of read() varies significantly depending on the type of
+ *   file descriptor:
+ *
+ *   - Regular files: Reads from current position, advances position, returns 0
+ *     at EOF. Short reads are rare but possible near EOF or on signal.
+ *
+ *   - Pipes and FIFOs: Blocking by default. Returns available data (up to count)
+ *     or blocks until data is available. Returns 0 when all writers have closed.
+ *     O_NONBLOCK returns EAGAIN when empty instead of blocking.
+ *
+ *   - Sockets: Similar to pipes. Specific behavior depends on socket type and
+ *     protocol. MSG_* flags can be specified via recv() for more control.
+ *
+ *   - Terminals: Line-buffered in canonical mode; read returns when newline is
+ *     entered or buffer is full. Raw mode returns immediately when data available.
+ *     Special handling for signals (SIGINT on Ctrl+C, etc.).
+ *
+ *   - Device special files: Behavior is device-specific. Some devices support
+ *     seeking, others do not. Read size may be constrained by device.
+ *
+ *   Race condition: Concurrent reads from the same file description (not just
+ *   file descriptor) can race on the file position. Linux 3.14+ provides atomic
+ *   position updates for regular files via f_pos_lock, but applications should
+ *   use pread() for concurrent positioned reads.
+ *
+ *   O_DIRECT reads bypass the page cache and typically require aligned buffers
+ *   and positions. Alignment requirements are filesystem-specific; use statx()
+ *   with STATX_DIOALIGN (Linux 6.1+) to query. Unaligned O_DIRECT reads fail
+ *   with EINVAL on most filesystems.
+ *
+ *   For splice(2)-like zero-copy reads, consider using splice(), sendfile(),
+ *   or copy_file_range() instead of read() + write().
+ */
 SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
 {
 	return ksys_read(fd, buf, count);
@@ -744,6 +1041,419 @@ ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
 	return ret;
 }
 
+/**
+ * sys_write - Write data to a file descriptor
+ * @fd: File descriptor to write to
+ * @buf: User-space buffer containing data to write
+ * @count: Maximum number of bytes to write
+ *
+ * long-desc: Writes at most count bytes from the user buffer buf to fd. For
+ *   seekable files (regular files, block devices), the write begins at the
+ *   current file offset, and the file offset is advanced by the number of
+ *   bytes written. If the file was opened with O_APPEND, the file offset is
+ *   first set to the end of the file before writing. For stream files
+ *   (FMODE_STREAM, such as pipes, FIFOs, and sockets), the file offset is not
+ *   used and writing occurs at the position defined by the device. Other
+ *   files, including many character devices, keep a file offset although a
+ *   driver may ignore it.
+ *
+ *   Fewer than count bytes may be written, for example when the filesystem
+ *   runs out of space, the write reaches RLIMIT_FSIZE, or a signal arrives
+ *   after some data was written. The caller writes the rest with another
+ *   write() call.
+ *
+ *   On Linux, write() transfers at most MAX_RW_COUNT (INT_MAX & PAGE_MASK,
+ *   0x7ffff000 with 4KB pages, just under 2GB) bytes per call, regardless of
+ *   whether the file or filesystem would allow more. This prevents signed
+ *   arithmetic overflow.
+ *
+ *   For regular files, a successful write() does not guarantee that data has been
+ *   committed to disk. Use fsync(2) or fdatasync(2) if durability is required.
+ *   For O_SYNC or O_DSYNC files, the kernel automatically syncs data on write.
+ *
+ *   POSIX permits writes that are interrupted after partial writes to either
+ *   return -1 with errno=EINTR, or to return the count of bytes already written.
+ *   Linux implements the latter behavior: if some data has been written before
+ *   a signal arrives, write() returns the number of bytes written rather than
+ *   failing with EINTR.
+ *
+ * contexts: process, sleepable
+ *
+ * param: fd
+ *   type: fd, input
+ *   constraint-type: range(0, INT_MAX)
+ *   cdesc: Must be a valid, open file descriptor with write permission.
+ *     The file must have been opened with O_WRONLY or O_RDWR. File descriptors
+ *     opened with O_RDONLY, O_PATH, or that have been closed return EBADF.
+ *     Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are valid if
+ *     open and writable. AT_FDCWD and other special values are not valid.
+ *
+ * param: buf
+ *   type: user_ptr, input
+ *   constraint-type: buffer(2)
+ *   cdesc: Must point to a readable user-space region of at least count bytes.
+ *     The range is checked via access_ok() and a range outside user space
+ *     fails with EFAULT. NULL is not rejected by that check. With a count of 0
+ *     the call returns 0 in common cases. With a count above 0 the copy from
+ *     user space fails with EFAULT, or the call returns a short count if part
+ *     of the buffer was copied first. O_DIRECT writes may require block-size
+ *     alignment (see STATX_DIOALIGN).
+ *
+ * param: count
+ *   type: uint, input
+ *   cdesc: Maximum number of bytes to write. The range buf to buf + count must
+ *     lie within user space or access_ok() fails with EFAULT, which includes
+ *     counts that do not fit in ssize_t on 64-bit kernels. On 32-bit kernels
+ *     such a count reaches rw_verify_area() and fails with EINVAL. A count of
+ *     0 is passed to the file's write method, which typically returns 0 but
+ *     may trigger driver-specific side effects. After the checks pass, counts
+ *     above MAX_RW_COUNT (INT_MAX & PAGE_MASK, 0x7ffff000 with 4KB pages) are
+ *     clamped.
+ *
+ * return:
+ *   type: int
+ *   check-type: range
+ *   success: >= 0
+ *   desc: On success, returns the number of bytes written (non-negative). Zero
+ *     indicates that nothing was written (count was 0, or a device-specific
+ *     write method accepted no data). Non-blocking writes that cannot proceed
+ *     return -EAGAIN instead. The return value may be less than count due to
+ *     resource limits, signal interruption, or device constraints (short
+ *     write). On error, returns a negative error code.
+ *
+ * error: EBADF, Bad file descriptor
+ *   desc: fd is not a valid file descriptor, or fd was not opened for writing.
+ *     This includes file descriptors opened with O_RDONLY, O_PATH, or file
+ *     descriptors that have been closed. Also returned if the file structure
+ *     does not have FMODE_WRITE set.
+ *
+ * error: EFAULT, Bad address
+ *   desc: buf points outside the accessible address space. The buffer address
+ *     failed access_ok() validation. Can also occur if a fault happens during
+ *     copy_from_user() when reading data from user space.
+ *
+ * error: EINVAL, Invalid argument
+ *   desc: Returned in several cases: (1) The file has no write or write_iter
+ *     method (FMODE_CAN_WRITE is not set). (2) The file was opened with
+ *     O_DIRECT and the buffer alignment, offset, or count does not meet the
+ *     filesystem's alignment requirements. (3) rw_verify_area() rejects a
+ *     negative file position, or a position plus count that overflows, on a
+ *     file without FOP_UNSIGNED_OFFSET. (4) The count, cast to ssize_t, is
+ *     negative (32-bit kernels only, as 64-bit kernels fail access_ok() first
+ *     with EFAULT).
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ *   desc: fd refers to a file (pipe, socket, device) that is marked non-blocking
+ *     (O_NONBLOCK) and the write would block because the buffer is full.
+ *     Equivalent to EWOULDBLOCK. The application should retry later or use
+ *     select/poll/epoll to wait for writability.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: The call was interrupted by a signal before any data was written. This
+ *     only occurs if no data has been transferred; if some data was written
+ *     before the signal, the call returns the number of bytes written. The
+ *     caller should typically restart the write.
+ *
+ * error: EPIPE, Broken pipe
+ *   desc: fd refers to a pipe or socket whose reading end has been closed.
+ *     When this condition occurs, the calling process also receives a SIGPIPE
+ *     signal. If the signal is caught or ignored, EPIPE is still returned.
+ *     For sockets, MSG_NOSIGNAL (via send()) suppresses the signal. For
+ *     pwritev2(), the RWF_NOSIGNAL flag suppresses it.
+ *
+ * error: EFBIG, File too large
+ *   desc: The write starts at or beyond a file size limit. The generic write
+ *     path returns EFBIG when the starting position is at or beyond
+ *     RLIMIT_FSIZE, in which case the process also receives SIGXFSZ, or at or
+ *     beyond the maximum file size of the filesystem. Without O_LARGEFILE
+ *     (32-bit systems only) that maximum is 2GB minus one byte. A write that
+ *     starts below a limit but would cross it is shortened to end at the
+ *     limit.
+ *
+ * error: ENOSPC, No space left on device
+ *   desc: The device containing the file has no room for the data. This can
+ *     occur mid-write resulting in a short write followed by ENOSPC on retry.
+ *
+ * error: EDQUOT, Disk quota exceeded
+ *   desc: The user's quota of disk blocks on the filesystem has been exhausted.
+ *     Like ENOSPC, this can result in a short write.
+ *
+ * error: EIO, Input/output error
+ *   desc: A low-level I/O error occurred while modifying the inode or writing
+ *     data. This typically indicates hardware failure, filesystem corruption,
+ *     or network filesystem timeout. Some data may have been written.
+ *
+ * error: EPERM, Operation not permitted
+ *   desc: The operation was prevented (1) by a file seal (F_SEAL_WRITE or
+ *     F_SEAL_FUTURE_WRITE on memfd/shmem, or F_SEAL_GROW for a write that
+ *     extends the file), (2) by a filesystem-specific check, for example ext4
+ *     refuses writes to an immutable inode, (3) because the file is a
+ *     read-only block device, (4) by an LSM hook denying the operation, or (5)
+ *     by a fanotify pre-content (FAN_PRE_ACCESS) listener denying the write.
+ *
+ * error: EOVERFLOW, Value too large for defined data type
+ *   desc: Returned by rw_verify_area() only for files with FOP_UNSIGNED_OFFSET
+ *     (for example /proc/pid/mem) when the file position is negative, meaning
+ *     above LLONG_MAX, and count is at least -pos so that the write would wrap
+ *     past the end of the 64-bit offset space. Files without
+ *     FOP_UNSIGNED_OFFSET get EINVAL for a negative position or a position
+ *     plus count that overflows. Exceeding filesystem file size limits is
+ *     EFBIG, not EOVERFLOW.
+ *
+ * error: EDESTADDRREQ, Destination address required
+ *   desc: fd is a datagram socket for which no peer address has been set using
+ *     connect(2). Use sendto(2) to specify the destination address.
+ *
+ * error: ETXTBSY, Text file busy
+ *   desc: The file is being used as a swap file (IS_SWAPFILE).
+ *
+ * error: EXDEV, Cross-device link
+ *   desc: When writing to a pipe that has been configured as a watch queue
+ *     (CONFIG_WATCH_QUEUE), direct write() calls are not supported.
+ *
+ * error: ENOMEM, Out of memory
+ *   desc: Insufficient kernel memory was available for the write operation.
+ *     For pipes, this occurs when allocating pages for the pipe buffer.
+ *
+ * error: ERESTARTSYS, Restart system call (internal)
+ *   desc: Internal error code indicating the syscall should be restarted. This
+ *     is converted to EINTR if SA_RESTART is not set on the signal handler, or
+ *     the syscall is transparently restarted if SA_RESTART is set. User space
+ *     should not see this error code directly.
+ *
+ * error: EACCES, Permission denied
+ *   desc: The security subsystem (LSM such as SELinux or AppArmor) denied the
+ *     write operation via security_file_permission(). This can occur even if
+ *     the file was successfully opened.
+ *
+ * lock: file->f_pos_lock
+ *   type: mutex
+ *   acquired: true
+ *   released: true
+ *   desc: For regular files that require atomic position updates (FMODE_ATOMIC_POS),
+ *     the f_pos_lock mutex is acquired by fdget_pos() at syscall entry and released
+ *     by fdput_pos() at syscall exit. This serializes concurrent writes sharing
+ *     the same file description. Not acquired for stream files (FMODE_STREAM like
+ *     pipes and sockets) or when the file is not shared.
+ *
+ * lock: sb->s_writers (freeze protection)
+ *   type: custom
+ *   acquired: true
+ *   released: true
+ *   desc: For regular files, file_start_write() acquires freeze protection on
+ *     the superblock via sb_start_write() before the write, and file_end_write()
+ *     releases it after. This prevents writes during filesystem freeze. Not
+ *     acquired for non-regular files (pipes, sockets, devices).
+ *
+ * lock: inode->i_rwsem
+ *   type: semaphore
+ *   acquired: true
+ *   released: true
+ *   desc: For regular files using generic_file_write_iter(), the inode's i_rwsem
+ *     is acquired in write mode before modifying file data. This is internal to
+ *     the filesystem and released before return. Not all filesystems use this
+ *     pattern.
+ *
+ * lock: pipe->mutex
+ *   type: mutex
+ *   acquired: true
+ *   released: true
+ *   desc: For pipes and FIFOs, the pipe's mutex is held while modifying pipe
+ *     buffers. Released temporarily while waiting for space, then reacquired.
+ *
+ * lock: RCU read-side
+ *   type: rcu
+ *   acquired: true
+ *   released: true
+ *   desc: Taken only when the files_struct is shared with other threads, in
+ *     which case the fd lookup in fdget() uses RCU and takes a file reference.
+ *     A private table needs no RCU. The RCU read lock is acquired and released
+ *     internally by the fd lookup path, not held across the entire syscall.
+ *     fdput() releases the file reference count, not the RCU lock.
+ *
+ * signal: SIGPIPE
+ *   number: SIGPIPE
+ *   direction: send
+ *   action: terminate
+ *   condition: Writing to a pipe or socket with no readers
+ *   desc: When writing to a pipe whose read end is closed, or a socket whose
+ *     peer has closed, SIGPIPE is sent to the calling process. The default
+ *     action terminates the process. Use signal(SIGPIPE, SIG_IGN) to suppress
+ *     for write(). EPIPE is returned regardless of signal disposition.
+ *   timing: during
+ *
+ * signal: SIGXFSZ
+ *   number: SIGXFSZ
+ *   direction: send
+ *   action: coredump
+ *   condition: Write starts at or beyond RLIMIT_FSIZE
+ *   desc: When a write starts at or beyond the soft file size limit
+ *     (RLIMIT_FSIZE), generic_write_check_limits() sends SIGXFSZ and the write
+ *     returns EFBIG. The default action terminates with a core dump. A write
+ *     that starts below the limit but would cross it is shortened to end at
+ *     the limit, with no signal. If RLIMIT_FSIZE is RLIM_INFINITY, no signal
+ *     is sent.
+ *   timing: during
+ *
+ * signal: Any signal
+ *   direction: receive
+ *   action: return
+ *   condition: While blocked waiting for space (pipes, sockets)
+ *   desc: The syscall may be interrupted by signals while waiting for buffer
+ *     space to become available. If interrupted before any data is written,
+ *     returns -EINTR or -ERESTARTSYS. If data was already written, returns the
+ *     byte count. Restartable if SA_RESTART is set and no data was written.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: yes
+ *
+ * side-effect: file_position
+ *   target: file->f_pos
+ *   condition: For seekable files when write succeeds (returns > 0)
+ *   desc: The file offset (f_pos) is advanced by the number of bytes written.
+ *     For files opened with O_APPEND, f_pos is first set to file size. For
+ *     stream files (FMODE_STREAM such as pipes and sockets), the offset is not
+ *     used or modified. Position updates are protected by f_pos_lock when
+ *     shared.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: inode timestamps (mtime, ctime)
+ *   condition: Before the data is copied, for a non-zero count on the generic
+ *     write path
+ *   desc: Updates the file's modification time (mtime) and change time (ctime)
+ *     via file_update_time(), which runs before the data is copied, so the
+ *     timestamps can change even if the write then fails or is short. The
+ *     update is skipped for inodes flagged NOCMTIME and when the timestamps
+ *     are unchanged. The timestamp precision depends on whether the filesystem
+ *     type sets FS_MGTIME (multigrain timestamps).
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: SUID/SGID bits (mode) and file capabilities
+ *   condition: Write to a regular file that has S_ISUID, S_ISGID, or file
+ *     capabilities set, with a non-zero count
+ *   desc: file_remove_privs() clears S_ISUID, and clears S_ISGID when S_IXGRP
+ *     is also set or the caller is not in the file's group, unless the caller
+ *     has CAP_FSETID. File capabilities (the security.capability xattr) are
+ *     removed as well, regardless of CAP_FSETID. This is a security feature to
+ *     prevent privilege escalation via modified setuid binaries. It runs before
+ *     the data is copied.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: file data
+ *   condition: When write succeeds (returns > 0)
+ *   desc: Modifies the file's data content. For regular files, data is written
+ *     to the page cache (buffered I/O) or directly to storage (O_DIRECT).
+ *     Data is not guaranteed to be persistent until fsync() or fdatasync()
+ *     completes, or writeback has written it to storage.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: task I/O accounting
+ *   condition: When CONFIG_TASK_XACCT is enabled and the write method is
+ *     invoked
+ *   desc: Updates the current task's I/O accounting statistics. The wchar field
+ *     (write characters) is incremented by bytes written via add_wchar() only on
+ *     successful writes (ret > 0). The syscw field (syscall write count) is
+ *     incremented via inc_syscw() once the write method has been invoked, even
+ *     if it fails, but not when the FMODE_WRITE, FMODE_CAN_WRITE, access_ok()
+ *     or rw_verify_area() checks fail. These statistics are visible in
+ *     /proc/[pid]/io.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: fsnotify events
+ *   condition: When write returns > 0
+ *   desc: Generates an FS_MODIFY fsnotify event via fsnotify_modify(), allowing
+ *     inotify, fanotify, and dnotify watchers to be notified of the write.
+ *
+ * capability: CAP_FSETID
+ *   type: bypass_check
+ *   allows: Keep the SUID and SGID bits set when the file is written
+ *   without: SUID is cleared on write, and SGID is cleared if S_IXGRP is set
+ *     or the caller is not in the file's group
+ *   condition: Checked in setattr_should_drop_suidgid() during
+ *     file_remove_privs()
+ *
+ * constraint: MAX_RW_COUNT
+ *   desc: After the access_ok() and rw_verify_area() checks pass, the count
+ *     parameter is silently clamped to MAX_RW_COUNT (INT_MAX & PAGE_MASK, just
+ *     under 2GB) to prevent integer overflow in internal calculations. This is
+ *     transparent to the caller.
+ *   expr: actual_count = min(count, MAX_RW_COUNT)
+ *
+ * constraint: File must be open for writing
+ *   desc: The file descriptor must have been opened with O_WRONLY or O_RDWR.
+ *     Files opened with O_RDONLY or O_PATH cannot be written and return EBADF.
+ *     The file must have both FMODE_WRITE and FMODE_CAN_WRITE flags set.
+ *   expr: (file->f_mode & FMODE_WRITE) && (file->f_mode & FMODE_CAN_WRITE)
+ *
+ * constraint: RLIMIT_FSIZE
+ *   desc: The size of data written is constrained by the RLIMIT_FSIZE resource
+ *     limit. In generic_write_check_limits(), a write that starts at or beyond
+ *     the limit sends SIGXFSZ and returns EFBIG. A write that starts below the
+ *     limit but would cross it is shortened to limit - pos bytes, with no
+ *     signal.
+ *   expr: pos < rlimit(RLIMIT_FSIZE) || rlimit(RLIMIT_FSIZE) == RLIM_INFINITY
+ *
+ * constraint: File seals
+ *   desc: For memfd or shmem files with F_SEAL_WRITE or F_SEAL_FUTURE_WRITE
+ *     seals applied, all write operations fail with EPERM. With F_SEAL_GROW,
+ *     writes that would extend file size fail with EPERM.
+ *
+ * examples: n = write(fd, buf, sizeof(buf));  // Basic write
+ *   n = write(STDOUT_FILENO, msg, strlen(msg));  // Write to stdout
+ *   // Handle short writes:
+ *   while (total < len) {
+ *     n = write(fd, buf + total, len - total);
+ *     if (n < 0) break;
+ *     total += n;
+ *   }
+ *   // Pipe error handling:
+ *   if (write(pipefd[1], &byte, 1) < 0 && errno == EPIPE)
+ *     handle_broken_pipe();
+ *
+ * notes: The behavior of write() varies significantly depending on the type of
+ *   file descriptor:
+ *
+ *   - Regular files: Writes to the page cache (buffered) or directly to storage
+ *     (O_DIRECT). Short writes are rare except near RLIMIT_FSIZE or disk full.
+ *     O_APPEND is atomic for determining write position.
+ *
+ *   - Pipes and FIFOs: Blocking by default. Writes up to PIPE_BUF (4096 bytes
+ *     on Linux) are guaranteed atomic. Larger writes may be interleaved with
+ *     writes from other processes. Blocks if pipe is full; returns EAGAIN with
+ *     O_NONBLOCK. SIGPIPE/EPIPE if no readers.
+ *
+ *   - Sockets: Behavior depends on socket type and protocol. Stream sockets
+ *     (TCP) may return partial writes. Datagram sockets (UDP) typically write
+ *     complete messages or fail. SIGPIPE/EPIPE for broken connections (unless
+ *     MSG_NOSIGNAL). EDESTADDRREQ for unconnected datagram sockets.
+ *
+ *   - Terminals: May block on flow control. Canonical vs raw mode affects
+ *     behavior. Special characters may be interpreted.
+ *
+ *   - Device special files: Behavior is device-specific. Block devices behave
+ *     similarly to regular files. Character device behavior varies.
+ *
+ *   Race condition considerations: Concurrent writes from threads sharing a
+ *   file description race on the file position. Linux 3.14+ provides atomic
+ *   position updates via f_pos_lock for regular files (FMODE_ATOMIC_POS), but
+ *   for maximum safety, use pwrite() for concurrent positioned writes.
+ *
+ *   O_DIRECT writes bypass the page cache and typically require buffer and
+ *   offset alignment to filesystem block size. Query requirements via statx()
+ *   with STATX_DIOALIGN (Linux 6.1+). Unaligned O_DIRECT writes return EINVAL
+ *   on most filesystems.
+ *
+ *   For zero-copy writes, consider using splice(2), sendfile(2), or vmsplice(2)
+ *   instead of copying data through user-space buffers with write().
+ *
+ *   Partial writes (short writes) must be handled by application code.
+ *   Applications should loop until all data is written or an error occurs.
+ */
 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
 		size_t, count)
 {
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/include/trace/events/kapi.h b/include/trace/events/kapi.h
new file mode 100644
index 0000000000000..47828f3338828
--- /dev/null
+++ b/include/trace/events/kapi.h
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM kapi
+
+#if !defined(_TRACE_KAPI_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_KAPI_H
+
+#include <linux/tracepoint.h>
+
+/* Max length of the rendered "name=value, ..." parameter list. */
+#define KAPI_TP_PARAMS_LEN 256
+
+/*
+ * Emitted from the CONFIG_KAPI_RUNTIME_CHECKS syscall validation path for
+ * syscalls that have a KAPI specification: kapi_syscall_enter fires before
+ * parameter validation, kapi_syscall_exit after the handler returns.
+ * @name is the spec name, e.g. "sys_open".
+ *
+ * kapi_syscall_enter carries both the raw argument values (args[]) and, when
+ * the spec provides parameter metadata, a rendered "name=value" list (params,
+ * built by the caller): pointer-like values in hex, integers and fds in decimal.
+ */
+TRACE_EVENT(kapi_syscall_enter,
+
+	TP_PROTO(const char *name, int nargs, const s64 *args, const char *params),
+
+	TP_ARGS(name, nargs, args, params),
+
+	TP_STRUCT__entry(
+		__string(	name,	name	)
+		__field(	int,	nargs	)
+		__array(	u64,	args,	6	)
+		__string(	params,	params	)
+	),
+
+	TP_fast_assign(
+		__assign_str(name);
+		__entry->nargs = nargs;
+		memset(__entry->args, 0, sizeof(__entry->args));
+		if (args && nargs > 0)
+			memcpy(__entry->args, args,
+			       min_t(int, nargs, 6) * sizeof(__entry->args[0]));
+		__assign_str(params);
+	),
+
+	TP_printk("%s(%s)", __get_str(name), __get_str(params))
+);
+
+TRACE_EVENT(kapi_syscall_exit,
+
+	TP_PROTO(const char *name, long ret, bool spec_match),
+
+	TP_ARGS(name, ret, spec_match),
+
+	TP_STRUCT__entry(
+		__string(	name,		name		)
+		__field(	long,		ret		)
+		__field(	bool,		spec_match	)
+	),
+
+	TP_fast_assign(
+		__assign_str(name);
+		__entry->ret = ret;
+		__entry->spec_match = spec_match;
+	),
+
+	TP_printk("%s = %ld spec_match=%d",
+		  __get_str(name), __entry->ret, __entry->spec_match)
+);
+
+#endif /* _TRACE_KAPI_H */
+
+/* This part must be outside protection */
+#include <trace/define_trace.h>
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..6075ab82bd231
--- /dev/null
+++ b/kernel/api/Kconfig
@@ -0,0 +1,74 @@
+# 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_SPEC_DEBUGFS
+	bool "Export kernel API specifications via debugfs"
+	depends on KAPI_SPEC
+	depends on DEBUG_FS
+	help
+	  This option enables exporting kernel API specifications through
+	  the debugfs filesystem. When enabled, specifications can be
+	  accessed at /sys/kernel/debug/kapi/.
+
+	  The debugfs interface provides:
+	  - A list of all available API specifications
+	  - Detailed information for each API including parameters,
+	    return values, errors, locking requirements, and constraints
+	  - Complete machine-readable representation of the specs
+
+	  This is useful for documentation tools, static analyzers, and
+	  runtime introspection of kernel APIs.
+
+	  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..2e1c3555d5aa5
--- /dev/null
+++ b/kernel/api/Makefile
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Makefile for the Kernel API Specification Framework
+#
+
+# Core API specification framework
+obj-y				+= kernel_api_spec.o
+
+# Debugfs interface for kernel API specs
+obj-$(CONFIG_KAPI_SPEC_DEBUGFS) += kapi_debugfs.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_debugfs.c b/kernel/api/kapi_debugfs.c
new file mode 100644
index 0000000000000..5d9c2649a0dc6
--- /dev/null
+++ b/kernel/api/kapi_debugfs.c
@@ -0,0 +1,553 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Kernel API specification debugfs interface
+ *
+ * This provides a debugfs interface to expose kernel API specifications
+ * at runtime, allowing tools and users to query the complete API specs.
+ */
+
+#include <linux/debugfs.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/seq_file.h>
+#include <linux/kernel_api_spec.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "internal.h"
+
+/* Helper to print parameter flags */
+static void print_param_flags(struct seq_file *m, u32 flags)
+{
+	seq_puts(m, "    flags: ");
+	if ((flags & KAPI_PARAM_INOUT) == KAPI_PARAM_INOUT)
+		seq_puts(m, "INOUT ");
+	else if (flags & KAPI_PARAM_IN)
+		seq_puts(m, "IN ");
+	else if (flags & KAPI_PARAM_OUT)
+		seq_puts(m, "OUT ");
+	if (flags & KAPI_PARAM_OPTIONAL)
+		seq_puts(m, "OPTIONAL ");
+	if (flags & KAPI_PARAM_CONST)
+		seq_puts(m, "CONST ");
+	if (flags & KAPI_PARAM_USER)
+		seq_puts(m, "USER ");
+	if (flags & KAPI_PARAM_VOLATILE)
+		seq_puts(m, "VOLATILE ");
+	if (flags & KAPI_PARAM_DMA)
+		seq_puts(m, "DMA ");
+	if (flags & KAPI_PARAM_ALIGNED)
+		seq_puts(m, "ALIGNED ");
+	seq_puts(m, "\n");
+}
+
+/* Helper to print context flags */
+static void print_context_flags(struct seq_file *m, u32 flags)
+{
+	seq_puts(m, "Context flags: ");
+	if (flags & KAPI_CTX_PROCESS)
+		seq_puts(m, "PROCESS ");
+	if (flags & KAPI_CTX_HARDIRQ)
+		seq_puts(m, "HARDIRQ ");
+	if (flags & KAPI_CTX_SOFTIRQ)
+		seq_puts(m, "SOFTIRQ ");
+	if (flags & KAPI_CTX_NMI)
+		seq_puts(m, "NMI ");
+	if (flags & KAPI_CTX_SLEEPABLE)
+		seq_puts(m, "SLEEPABLE ");
+	if (flags & KAPI_CTX_ATOMIC)
+		seq_puts(m, "ATOMIC ");
+	if (flags & KAPI_CTX_PREEMPT_DISABLED)
+		seq_puts(m, "PREEMPT_DISABLED ");
+	if (flags & KAPI_CTX_IRQ_DISABLED)
+		seq_puts(m, "IRQ_DISABLED ");
+	seq_puts(m, "\n");
+}
+
+/*
+ * Print a multi-line value with every line indented, so that no line of it
+ * can be mistaken for a field of its own.
+ */
+static void print_block(struct seq_file *m, const char *text)
+{
+	while (*text) {
+		size_t len = strcspn(text, "\n");
+
+		if (len)
+			seq_printf(m, "  %.*s", (int)len, text);
+		seq_putc(m, '\n');
+		text += len;
+		if (*text == '\n')
+			text++;
+	}
+}
+
+/* Helper to print a list of signed values */
+static void print_s64_list(struct seq_file *m, const char *label,
+			   const s64 *vals, u32 count)
+{
+	u32 i;
+
+	if (!vals || !count)
+		return;
+
+	seq_printf(m, "%s:", label);
+	for (i = 0; i < count; i++)
+		seq_printf(m, " %lld", vals[i]);
+	seq_puts(m, "\n");
+}
+
+/* Helper to print which parameter carries a buffer's element count */
+static void print_size_param(struct seq_file *m,
+			     const struct kernel_api_spec *spec,
+			     const struct kapi_param_spec *param)
+{
+	int idx = param->size_param_idx - 1;
+
+	if (idx < 0)
+		return;
+
+	seq_printf(m, "      size_param: %s (index %d)\n",
+		   idx < spec->param_count && idx < KAPI_MAX_PARAMS ?
+		   spec->params[idx].name : "?", idx);
+	if (param->size_multiplier)
+		seq_printf(m, "      size_multiplier: %zu\n",
+			   param->size_multiplier);
+}
+
+/* Show function for individual API spec */
+static int kapi_spec_show(struct seq_file *m, void *v)
+{
+	struct kernel_api_spec *spec = m->private;
+	int i;
+
+	seq_puts(m, "Kernel API Specification\n");
+	seq_puts(m, "========================\n\n");
+
+	/* Basic info */
+	seq_printf(m, "Name: %s\n", spec->name);
+	seq_printf(m, "Version: %u\n", spec->version);
+	seq_printf(m, "Description: %s\n", spec->description);
+	if (spec->long_description && *spec->long_description) {
+		seq_puts(m, "Long description:\n");
+		print_block(m, spec->long_description);
+	}
+
+	/* Context */
+	print_context_flags(m, spec->context_flags);
+	seq_puts(m, "\n");
+
+	/* Parameters */
+	if (spec->param_count > 0) {
+		seq_printf(m, "Parameters (%u):\n", spec->param_count);
+		for (i = 0; i < spec->param_count && i < KAPI_MAX_PARAMS; i++) {
+			struct kapi_param_spec *param = &spec->params[i];
+
+			seq_printf(m, "  [%d] %s:\n", i, param->name);
+			seq_printf(m, "    type: %s (%s)\n",
+				   kapi_param_type_to_string(param->type), param->type_name);
+			print_param_flags(m, param->flags);
+			if (param->description && *param->description)
+				seq_printf(m, "    description: %s\n", param->description);
+			if (param->size > 0)
+				seq_printf(m, "    size: %zu\n", param->size);
+			if (param->alignment > 0)
+				seq_printf(m, "    alignment: %zu\n", param->alignment);
+
+			/* Print constraints if any */
+			if (param->constraint_type != KAPI_CONSTRAINT_NONE ||
+			    (param->constraints && *param->constraints)) {
+				seq_puts(m, "    constraints:\n");
+				switch (param->constraint_type) {
+				case KAPI_CONSTRAINT_NONE:
+					break;
+				case KAPI_CONSTRAINT_RANGE:
+					seq_puts(m, "      type: range\n");
+					seq_printf(m, "      min: %lld\n", param->min_value);
+					seq_printf(m, "      max: %lld\n", param->max_value);
+					break;
+				case KAPI_CONSTRAINT_MASK:
+					seq_puts(m, "      type: mask\n");
+					seq_printf(m, "      valid_bits: 0x%llx\n",
+						   param->valid_mask);
+					break;
+				case KAPI_CONSTRAINT_ENUM:
+					seq_puts(m, "      type: enum\n");
+					seq_printf(m, "      count: %u\n", param->enum_count);
+					print_s64_list(m, "      values",
+						       param->enum_values,
+						       param->enum_count);
+					break;
+				case KAPI_CONSTRAINT_USER_STRING:
+					seq_puts(m, "      type: user_string\n");
+					seq_printf(m, "      min_len: %lld\n", param->min_value);
+					seq_printf(m, "      max_len: %lld\n", param->max_value);
+					break;
+				case KAPI_CONSTRAINT_USER_PATH:
+					seq_puts(m, "      type: user_path\n");
+					seq_puts(m, "      max_len: PATH_MAX (4096)\n");
+					break;
+				case KAPI_CONSTRAINT_USER_PTR:
+					seq_puts(m, "      type: user_ptr\n");
+					seq_printf(m, "      size: %zu bytes\n", param->size);
+					print_size_param(m, spec, param);
+					break;
+				case KAPI_CONSTRAINT_BUFFER:
+					seq_puts(m, "      type: buffer\n");
+					print_size_param(m, spec, param);
+					break;
+				case KAPI_CONSTRAINT_ALIGNMENT:
+					seq_puts(m, "      type: alignment\n");
+					seq_printf(m, "      alignment: %zu\n", param->alignment);
+					break;
+				case KAPI_CONSTRAINT_POWER_OF_TWO:
+					seq_puts(m, "      type: power_of_two\n");
+					break;
+				case KAPI_CONSTRAINT_PAGE_ALIGNED:
+					seq_puts(m, "      type: page_aligned\n");
+					break;
+				case KAPI_CONSTRAINT_NONZERO:
+					seq_puts(m, "      type: nonzero\n");
+					break;
+				case KAPI_CONSTRAINT_CUSTOM:
+					seq_puts(m, "      type: custom\n");
+					break;
+				default:
+					seq_printf(m, "      type: unknown (%d)\n",
+						   param->constraint_type);
+					break;
+				}
+				if (param->constraints && *param->constraints)
+					seq_printf(m, "      description: %s\n",
+						   param->constraints);
+			}
+			seq_puts(m, "\n");
+		}
+	}
+
+	/* Return value */
+	seq_puts(m, "Return value:\n");
+	seq_printf(m, "  type: %s\n", spec->return_spec.type_name);
+	if (spec->return_spec.description && *spec->return_spec.description)
+		seq_printf(m, "  description: %s\n", spec->return_spec.description);
+
+	switch (spec->return_spec.check_type) {
+	case KAPI_RETURN_EXACT:
+		seq_printf(m, "  success: == %lld\n", spec->return_spec.success_value);
+		break;
+	case KAPI_RETURN_RANGE:
+		seq_printf(m, "  success: [%lld, %lld]\n",
+			   spec->return_spec.success_min,
+			   spec->return_spec.success_max);
+		break;
+	case KAPI_RETURN_FD:
+		seq_puts(m, "  success: valid file descriptor (>= 0)\n");
+		break;
+	case KAPI_RETURN_ERROR_CHECK:
+		seq_puts(m, "  success: error check\n");
+		print_s64_list(m, "  error values",
+			       spec->return_spec.error_values,
+			       spec->return_spec.error_count);
+		break;
+	case KAPI_RETURN_CUSTOM:
+		seq_puts(m, "  success: custom check\n");
+		break;
+	case KAPI_RETURN_NO_RETURN:
+		seq_puts(m, "  success: does not return\n");
+		break;
+	default:
+		break;
+	}
+	seq_puts(m, "\n");
+
+	/* Errors */
+	if (spec->error_count > 0) {
+		seq_printf(m, "Errors (%u):\n", spec->error_count);
+		for (i = 0; i < spec->error_count && i < KAPI_MAX_ERRORS; i++) {
+			struct kapi_error_spec *err = &spec->errors[i];
+
+			seq_printf(m, "  %s (%d): %s\n",
+				   err->name, err->error_code, err->description);
+			if (err->condition && *err->condition)
+				seq_printf(m, "    condition: %s\n", err->condition);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Locks */
+	if (spec->lock_count > 0) {
+		seq_printf(m, "Locks (%u):\n", spec->lock_count);
+		for (i = 0; i < spec->lock_count && i < KAPI_MAX_LOCKS; i++) {
+			struct kapi_lock_spec *lock = &spec->locks[i];
+
+			seq_printf(m, "  %s (%s): %s\n", lock->lock_name,
+				   kapi_lock_type_to_string(lock->lock_type),
+				   lock->description);
+			seq_printf(m, "    scope: %s\n",
+				   kapi_lock_scope_to_string(lock->scope));
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Constraints */
+	if (spec->constraint_count > 0) {
+		seq_printf(m, "Additional constraints (%u):\n", spec->constraint_count);
+		for (i = 0; i < spec->constraint_count && i < KAPI_MAX_CONSTRAINTS; i++) {
+			struct kapi_constraint_spec *cons = &spec->constraints[i];
+
+			seq_printf(m, "  - %s", cons->name);
+			if (cons->description && *cons->description)
+				seq_printf(m, ": %s", cons->description);
+			seq_puts(m, "\n");
+			if (cons->expression && *cons->expression)
+				seq_printf(m, "    expression: %s\n", cons->expression);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Signals */
+	if (spec->signal_count > 0) {
+		seq_printf(m, "Signal handling (%u):\n", spec->signal_count);
+		for (i = 0; i < spec->signal_count && i < KAPI_MAX_SIGNALS; i++) {
+			struct kapi_signal_spec *sig = &spec->signals[i];
+
+			seq_printf(m, "  %s (%d):\n", sig->signal_name, sig->signal_num);
+			seq_puts(m, "    direction: ");
+			if (sig->direction & KAPI_SIGNAL_SEND)
+				seq_puts(m, "send ");
+			if (sig->direction & KAPI_SIGNAL_RECEIVE)
+				seq_puts(m, "receive ");
+			if (sig->direction & KAPI_SIGNAL_HANDLE)
+				seq_puts(m, "handle ");
+			if (sig->direction & KAPI_SIGNAL_BLOCK)
+				seq_puts(m, "block ");
+			if (sig->direction & KAPI_SIGNAL_IGNORE)
+				seq_puts(m, "ignore ");
+			seq_puts(m, "\n");
+			seq_puts(m, "    action: ");
+			switch (sig->action) {
+			case KAPI_SIGNAL_ACTION_DEFAULT:
+				seq_puts(m, "default");
+				break;
+			case KAPI_SIGNAL_ACTION_TERMINATE:
+				seq_puts(m, "terminate");
+				break;
+			case KAPI_SIGNAL_ACTION_COREDUMP:
+				seq_puts(m, "coredump");
+				break;
+			case KAPI_SIGNAL_ACTION_STOP:
+				seq_puts(m, "stop");
+				break;
+			case KAPI_SIGNAL_ACTION_CONTINUE:
+				seq_puts(m, "continue");
+				break;
+			case KAPI_SIGNAL_ACTION_CUSTOM:
+				seq_puts(m, "custom");
+				break;
+			case KAPI_SIGNAL_ACTION_RETURN:
+				seq_puts(m, "return");
+				break;
+			case KAPI_SIGNAL_ACTION_RESTART:
+				seq_puts(m, "restart");
+				break;
+			case KAPI_SIGNAL_ACTION_QUEUE:
+				seq_puts(m, "queue");
+				break;
+			case KAPI_SIGNAL_ACTION_DISCARD:
+				seq_puts(m, "discard");
+				break;
+			case KAPI_SIGNAL_ACTION_TRANSFORM:
+				seq_puts(m, "transform");
+				break;
+			default:
+				seq_puts(m, "unknown");
+				break;
+			}
+			seq_puts(m, "\n");
+			if (sig->description && *sig->description)
+				seq_printf(m, "    description: %s\n", sig->description);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Side effects */
+	if (spec->side_effect_count > 0) {
+		seq_printf(m, "Side effects (%u):\n", spec->side_effect_count);
+		for (i = 0; i < spec->side_effect_count && i < KAPI_MAX_SIDE_EFFECTS; i++) {
+			const struct kapi_side_effect *eff = &spec->side_effects[i];
+
+			seq_printf(m, "  - %s", eff->target);
+			if (eff->description && *eff->description)
+				seq_printf(m, ": %s", eff->description);
+			if (eff->reversible)
+				seq_puts(m, " (reversible)");
+			seq_puts(m, "\n");
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* State transitions */
+	if (spec->state_trans_count > 0) {
+		seq_printf(m, "State transitions (%u):\n", spec->state_trans_count);
+		for (i = 0; i < spec->state_trans_count && i < KAPI_MAX_STATE_TRANS; i++) {
+			const struct kapi_state_transition *trans = &spec->state_transitions[i];
+
+			seq_printf(m, "  %s: %s -> %s\n", trans->object,
+				   trans->from_state, trans->to_state);
+			if (trans->description && *trans->description)
+				seq_printf(m, "    %s\n", trans->description);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Capabilities */
+	if (spec->capability_count > 0) {
+		seq_printf(m, "Capabilities (%u):\n", spec->capability_count);
+		for (i = 0; i < spec->capability_count && i < KAPI_MAX_CAPABILITIES; i++) {
+			const struct kapi_capability_spec *cap = &spec->capabilities[i];
+
+			seq_printf(m, "  %s (%d):\n", cap->cap_name,
+				   cap->capability);
+			if (cap->allows && *cap->allows)
+				seq_printf(m, "    allows: %s\n", cap->allows);
+			if (cap->without_cap && *cap->without_cap)
+				seq_printf(m, "    without: %s\n", cap->without_cap);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Additional info */
+	if (spec->examples && *spec->examples) {
+		seq_puts(m, "Examples:\n");
+		print_block(m, spec->examples);
+		seq_putc(m, '\n');
+	}
+	if (spec->notes && *spec->notes) {
+		seq_puts(m, "Notes:\n");
+		print_block(m, spec->notes);
+		seq_putc(m, '\n');
+	}
+
+	return 0;
+}
+
+static int kapi_spec_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, kapi_spec_show, inode->i_private);
+}
+
+static const struct file_operations kapi_spec_fops = {
+	.open = kapi_spec_open,
+	.read = seq_read,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+/*
+ * JSON view of a single spec.  kapi_export_json() writes straight into the
+ * seq_file buffer; when the output does not fit, flagging an overflow makes
+ * seq_file retry with a larger buffer, so no fixed size limit applies.
+ */
+static int kapi_spec_json_show(struct seq_file *m, void *v)
+{
+	const struct kernel_api_spec *spec = m->private;
+	size_t size;
+	char *buf;
+	int ret;
+
+	if (!spec)
+		return -EINVAL;
+
+	size = seq_get_buf(m, &buf);
+	ret = size ? kapi_export_json(spec, buf, size) : -E2BIG;
+	if (ret == -E2BIG) {
+		seq_commit(m, -1);
+		return 0;
+	}
+	if (ret < 0)
+		return ret;
+
+	seq_commit(m, ret);
+	return 0;
+}
+
+static int kapi_spec_json_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, kapi_spec_json_show, inode->i_private);
+}
+
+static const struct file_operations kapi_spec_json_fops = {
+	.open = kapi_spec_json_open,
+	.read = seq_read,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+/*
+ * Show all available API specs.
+ *
+ * Note: This only iterates the static .kapi_specs section. Specs registered
+ * dynamically via kapi_register_spec() are not included in this listing
+ * or in the per-spec debugfs files.
+ */
+static int kapi_list_show(struct seq_file *m, void *v)
+{
+	const struct kernel_api_spec * const *pp;
+	int count = 0;
+
+	seq_puts(m, "Available Kernel API Specifications\n");
+	seq_puts(m, "===================================\n\n");
+
+	for (pp = __start_kapi_specs; pp < __stop_kapi_specs; pp++) {
+		const struct kernel_api_spec *spec = *pp;
+
+		if (!spec)
+			continue;
+		seq_printf(m, "%s - %s\n", spec->name, spec->description);
+		count++;
+	}
+
+	seq_printf(m, "\nTotal: %d specifications\n", count);
+	return 0;
+}
+
+static int kapi_list_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, kapi_list_show, NULL);
+}
+
+static const struct file_operations kapi_list_fops = {
+	.open = kapi_list_open,
+	.read = seq_read,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int __init kapi_debugfs_init(void)
+{
+	const struct kernel_api_spec * const *pp;
+	struct dentry *root, *spec_dir, *json_dir;
+
+	root = debugfs_create_dir("kapi", NULL);
+	debugfs_create_file("list", 0444, root, NULL, &kapi_list_fops);
+	spec_dir = debugfs_create_dir("specs", root);
+	json_dir = debugfs_create_dir("specs-json", root);
+
+	for (pp = __start_kapi_specs; pp < __stop_kapi_specs; pp++) {
+		const struct kernel_api_spec *spec = *pp;
+
+		if (!spec || !spec->name)
+			continue;
+		debugfs_create_file(spec->name, 0444, spec_dir,
+				    (void *)spec, &kapi_spec_fops);
+		debugfs_create_file(spec->name, 0444, json_dir,
+				    (void *)spec, &kapi_spec_json_fops);
+	}
+
+	return 0;
+}
+
+/* Initialize as part of kernel, not as a module */
+fs_initcall(kapi_debugfs_init);
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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 50));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 100));
+
+	param.min_value = -10;
+	param.max_value = -1;
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, -10));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, -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(&param, 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(&param, -1));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 101));
+
+	param.min_value = -10;
+	param.max_value = -1;
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, -11));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 0));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 0x00));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 0x0F));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 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(&param, 0x100));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 1));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 2));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 4));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 8));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 0));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 3));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, PAGE_SIZE));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 2 * PAGE_SIZE));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 1));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 1));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, -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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 8));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 16));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 1));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 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(&param, 1));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 5));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 10));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 0));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 3));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 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(&param);
+
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, NULL, 0));
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, (void __user *)PAGE_SIZE, 0));
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, 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(&param);
+
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, NULL, 1));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, 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(&param);
+
+	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(&param, kernel_addr, 1));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, 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(&param);
+
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, (void __user *)PAGE_SIZE, 16));
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, 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(&param, near_top, PAGE_SIZE));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, 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(&param);
+	param.size_multiplier = 8;
+
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, (void __user *)PAGE_SIZE,
+						  (s64)(SIZE_MAX / 8 + 1)));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, (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(&param, near_top, PAGE_SIZE / 8));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, 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..993674ce67789
--- /dev/null
+++ b/kernel/api/kernel_api_spec.c
@@ -0,0 +1,1470 @@
+// 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
+
+#define CREATE_TRACE_POINTS
+#include <trace/events/kapi.h>
+
+/*
+ * Render a syscall's parameters as a "name=value, ..." string for the
+ * kapi_syscall_enter tracepoint.  Names come from the spec; pointer-like
+ * values are shown in hex, integers and file descriptors in decimal.
+ */
+static void kapi_trace_format_params(const struct kernel_api_spec *spec,
+				     const s64 *args, int nargs,
+				     char *buf, size_t size)
+{
+	int i, used = 0;
+
+	buf[0] = '\0';
+	/* Bound by the caller-supplied arg count; the spec arity may differ. */
+	for (i = 0; args && i < nargs && i < 6; i++) {
+		const char *name = "arg";
+		bool dec = false;
+
+		if (i < spec->param_count) {
+			const struct kapi_param_spec *ps = &spec->params[i];
+
+			if (ps->name)
+				name = ps->name;
+			dec = ps->type == KAPI_TYPE_INT || ps->type == KAPI_TYPE_FD;
+		}
+
+		used += scnprintf(buf + used, size - used, "%s%s=",
+				  i ? ", " : "", name);
+		if (dec)
+			used += scnprintf(buf + used, size - used, "%lld",
+					  (long long)args[i]);
+		else
+			used += scnprintf(buf + used, size - used, "0x%llx",
+					  (unsigned long long)args[i]);
+	}
+}
+
+/**
+ * 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, ret = 0;
+
+	if (!spec || !params)
+		return 0;
+
+	if (trace_kapi_syscall_enter_enabled()) {
+		char pbuf[KAPI_TP_PARAMS_LEN];
+
+		kapi_trace_format_params(spec, params, param_count, pbuf, sizeof(pbuf));
+		trace_kapi_syscall_enter(spec->name, param_count, params, pbuf);
+	}
+
+	/* 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);
+		ret = -EINVAL;
+		goto out;
+	}
+
+	/* 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 */
+				ret = -EINVAL;
+				goto out;
+			}
+		}
+	}
+
+out:
+	/*
+	 * Emit the exit event on the rejection path too (the wrapper
+	 * short-circuits the handler on a non-zero return), so every
+	 * kapi_syscall_enter has a matching kapi_syscall_exit.
+	 */
+	if (ret)
+		trace_kapi_syscall_exit(spec->name, ret, false);
+
+	return ret;
+}
+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)
+{
+	bool valid = true;
+
+	if (!spec)
+		return 0;
+
+	/* Validate against the return spec when one was defined */
+	if (spec->return_magic == KAPI_MAGIC_RETURN)
+		valid = kapi_validate_return_value(spec, retval);
+
+	trace_kapi_syscall_exit(spec->name, retval, valid);
+
+	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 */
diff --git a/mm/madvise.c b/mm/madvise.c
index eeee82cf2b3f4..4eb29922a2150 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -2018,6 +2018,555 @@ int do_madvise(struct mm_struct *mm, unsigned long start, size_t len_in, int beh
 	return error;
 }
 
+/**
+ * sys_madvise - Give advice about use of memory
+ * @start: Starting virtual address of the range to advise on
+ * @len_in: Length of the range in bytes
+ * @behavior: Advice (a MADV_* constant) the kernel should apply to the range
+ *
+ * long-desc: Provides the kernel with advice or directions about the address
+ *   range starting at start and extending for len_in bytes. The advice is
+ *   selected by behavior, which is one of the MADV_* constants defined in
+ *   <sys/mman.h>. The behaviors fall into three groups. The hint group
+ *   updates VMA flags (MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL,
+ *   MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP, MADV_WIPEONFORK,
+ *   MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE, MADV_HUGEPAGE,
+ *   MADV_NOHUGEPAGE). The immediate-action group performs work synchronously
+ *   while preserving page contents (MADV_WILLNEED, MADV_COLD, MADV_PAGEOUT,
+ *   MADV_POPULATE_READ, MADV_POPULATE_WRITE, MADV_COLLAPSE, MADV_GUARD_REMOVE,
+ *   MADV_SOFT_OFFLINE). The destructive group discards, replaces or
+ *   invalidates page contents (MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE,
+ *   MADV_REMOVE, MADV_GUARD_INSTALL, MADV_HWPOISON). MADV_GUARD_INSTALL
+ *   belongs to the destructive group because it zaps any existing pages in
+ *   the range before installing PTE guard markers.
+ *
+ *   start must be page-aligned; len_in is rounded up to the next page
+ *   boundary internally. Once those validation checks pass, a zero-length
+ *   range succeeds without performing work. The kernel rejects ranges that
+ *   wrap (start + PAGE_ALIGN(len_in) < start) and ranges where len_in is
+ *   non-zero but rounds up to zero. Address tagging bits are stripped
+ *   from start before VMA lookup for every behavior except MADV_HWPOISON
+ *   and MADV_SOFT_OFFLINE, which receive the raw start value because they
+ *   bypass the VMA walk entirely.
+ *
+ *   The kernel return value reports whether any error condition was
+ *   encountered, not whether the requested work was performed. The
+ *   relationship between the return code and the work done varies by
+ *   handler:
+ *
+ *     - Hint behaviors set or clear VMA flags; how the flag is used later
+ *       depends on the behavior. MADV_DONTFORK / MADV_DOFORK set or clear
+ *       VM_DONTCOPY, which dup_mmap() honors, so the child does not get the
+ *       mapping. MADV_MERGEABLE / MADV_UNMERGEABLE control whether KSM scans
+ *       the VMA. MADV_NOHUGEPAGE blocks the fault-time, MADV_COLLAPSE and
+ *       khugepaged THP paths for the VMA. MADV_HUGEPAGE makes the VMA
+ *       eligible for THP when the transparent_hugepage mode is "madvise" and
+ *       increases defrag effort; it does not force allocation, which still
+ *       depends on the global mode, VMA suitability, defrag GFP policy, and
+ *       allocation or memcg-charge success. MADV_WIPEONFORK does not wipe
+ *       pages at fork time; the child VMA's pages are not copied, and the
+ *       child sees zero-filled pages when it first touches them.
+ *       MADV_KEEPONFORK clears VM_WIPEONFORK. MADV_DONTDUMP / MADV_DODUMP set
+ *       or clear VM_DONTDUMP, although always_dump_vma() still includes gate,
+ *       vm_ops-named or arch-named VMAs in a core dump. MADV_NORMAL /
+ *       MADV_RANDOM / MADV_SEQUENTIAL set or clear VM_RAND_READ / VM_SEQ_READ,
+ *       which only steer read-ahead.
+ *
+ *     - Walk-and-skip handlers (MADV_COLD, MADV_PAGEOUT, MADV_FREE,
+ *       MADV_GUARD_REMOVE) traverse the range and silently skip pages or
+ *       PMDs that fail per-page preconditions (absent, special, device,
+ *       shared, non-LRU, unsplittable, locked, etc.), returning 0 even
+ *       when most or all pages were skipped.
+ *
+ *     - Bulk-backend handlers delegate the requested range to a single
+ *       backend call: MADV_DONTNEED and MADV_DONTNEED_LOCKED to
+ *       zap_vma_range_batched(), MADV_REMOVE to vfs_fallocate(),
+ *       MADV_WILLNEED on regular files to vfs_fadvise(). The backend's
+ *       return is propagated for MADV_REMOVE and discarded for
+ *       MADV_WILLNEED; DAX files short-circuit MADV_WILLNEED entirely.
+ *
+ *     - Stop-on-error handlers (MADV_POPULATE_READ, MADV_POPULATE_WRITE,
+ *       MADV_SOFT_OFFLINE) walk the range but surface the first per-page
+ *       failure as an errno (-EHWPOISON, -EFAULT, -ENOMEM, ...) rather
+ *       than skipping silently.
+ *
+ *     - Hybrid handlers combine modes: MADV_WILLNEED walks for anonymous
+ *       and shmem ranges but bulk-calls vfs_fadvise() for regular files;
+ *       MADV_COLLAPSE walks PMD-by-PMD and tracks the last scan failure
+ *       so transient skips coexist with terminal errors;
+ *       MADV_GUARD_INSTALL walks to install markers and re-walks after
+ *       zap_vma_range() to clear pre-existing pages, retrying up to
+ *       MAX_MADVISE_GUARD_RETRIES; MADV_HWPOISON walks pages but folds
+ *       memory_failure()'s -EOPNOTSUPP back to 0.
+ *
+ *   Applications that need to know whether a specific page was acted on
+ *   must verify the result through other means (e.g. /proc/[pid]/smaps,
+ *   page faults, read-after-write).
+ *
+ *   On success, madvise() returns 0; unlike read(2) and write(2) it has no
+ *   notion of partial completion at the syscall boundary. When the range
+ *   spans multiple VMAs, the kernel applies the advice to each in turn. For
+ *   the behaviors that walk VMAs (all except MADV_POPULATE_*, MADV_HWPOISON
+ *   and MADV_SOFT_OFFLINE), an unmapped gap inside the range causes the call
+ *   to return -ENOMEM after processing the mapped portions, rather than
+ *   aborting at the gap, although the walk stops at the first per-VMA error.
+ *   MADV_POPULATE_* stops at the first failure, and MADV_HWPOISON and
+ *   MADV_SOFT_OFFLINE bypass the walk, so an unmapped address gives -EFAULT.
+ *
+ *   POSIX defines posix_madvise(3) for a portable subset (POSIX_MADV_NORMAL,
+ *   _RANDOM, _SEQUENTIAL, _WILLNEED, _DONTNEED). Linux MADV_DONTNEED is
+ *   destructive: it discards the contents of the affected anonymous pages and
+ *   subsequent reads return zero. POSIX permits but does not require
+ *   destruction, so portable code that needs the POSIX semantics should use
+ *   posix_madvise(3) instead.
+ *
+ * contexts: process, sleepable
+ *
+ * param: start
+ *   type: uint, input
+ *   constraint-type: page_aligned
+ *   cdesc: Starting virtual address of the range. Must be aligned to
+ *     PAGE_SIZE. An unaligned start always returns -EINVAL, even when
+ *     len_in is zero. Address tag bits, where supported by the architecture,
+ *     are cleared via untagged_addr() before the range is interpreted, with
+ *     the exception of MADV_HWPOISON and MADV_SOFT_OFFLINE, which receive
+ *     the raw start value because they bypass the VMA walk.
+ *
+ * param: len_in
+ *   type: uint, input
+ *   cdesc: Length of the range in bytes. Internally rounded up to a multiple
+ *     of PAGE_SIZE. A len_in of 0 is accepted and the call is a no-op that
+ *     returns 0. A non-zero len_in that rounds up to 0 (i.e. wraps around)
+ *     returns -EINVAL, as does a range whose end (start + PAGE_ALIGN(len_in))
+ *     would wrap below start.
+ *
+ * param: behavior
+ *   type: int, input
+ *   cdesc: One of the MADV_* constants from <sys/mman.h>. See the long
+ *     description above for the full list and the three semantic groups
+ *     (hint, immediate-action, destructive). Behaviors gated by Kconfig
+ *     (KSM, transparent hugepage, memory failure) return -EINVAL when the
+ *     underlying support is disabled. A few architectures (notably alpha)
+ *     renumber values; portable code should always use the symbolic names.
+ *
+ * return:
+ *   type: int
+ *   check-type: exact
+ *   success: 0
+ *   desc: On success, returns 0. On error, returns a negative error code.
+ *     There is no partial-success indication; either the entire processed
+ *     range succeeded, or an error is returned and an unspecified prefix of
+ *     the range may have been advised.
+ *
+ * error: EINVAL, Invalid argument
+ *   desc: Invalid input (unknown or Kconfig-disabled MADV_*, unaligned start,
+ *     range wrap, non-zero len_in rounding up to zero) or a VMA filter.
+ *     DONTNEED/FREE reject VM_PFNMAP, VM_LOCKED (not DONTNEED_LOCKED) and
+ *     misaligned hugetlb; FREE non-anonymous; WIPEONFORK file or shared; REMOVE
+ *     VM_LOCKED or no file; COLD/PAGEOUT LOCKED/PFNMAP/HUGETLB; DOFORK
+ *     VM_SPECIAL; KEEPONFORK VM_DROPPABLE; DODUMP SPECIAL/DROPPABLE; GUARD_*
+ *     SPECIAL/HUGETLB (INSTALL also LOCKED); COLLAPSE if not possible;
+ *     POPULATE_* on bad permissions.
+ *
+ * error: ENOMEM, Cannot allocate memory
+ *   desc: For VMA-walking behaviors, a gap between mapped VMAs inside the range
+ *     gives -ENOMEM after the mapped subranges have been processed, unless a
+ *     per-VMA error ends the walk first. MADV_POPULATE_* stops at the first
+ *     failure and returns -ENOMEM when the region has no VMA or
+ *     faultin_page_range() exhausts memory. MADV_COLLAPSE returns -ENOMEM when
+ *     its struct collapse_control cannot be allocated, and when
+ *     madvise_collapse_errno() maps SCAN_ALLOC_HUGE_PAGE_FAIL (no hugepage
+ *     available) to it.
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ *   desc: For the VMA-flag-mutating behaviors, an internal -ENOMEM from VMA
+ *     splitting is translated to -EAGAIN before being returned to userspace,
+ *     advising the caller that a transient kernel resource shortage
+ *     prevented the update. Also returned by MADV_COLLAPSE via
+ *     madvise_collapse_errno() for transient scan failures (folio lock
+ *     contention, LRU isolation failure, dirty/writeback) where retrying
+ *     the call may succeed.
+ *
+ * error: EIO, Input/output error
+ *   desc: For MADV_REMOVE, an I/O error from the underlying filesystem's
+ *     FALLOC_FL_PUNCH_HOLE handler is propagated back as -EIO, and
+ *     MADV_SOFT_OFFLINE returns -EIO when soft_offline_page() cannot handle
+ *     the page. MADV_HWPOISON reports an unhandled page as -EBUSY; -EIO can
+ *     reach it only from a ZONE_DEVICE pagemap's ->memory_failure()
+ *     callback. MADV_WILLNEED and MADV_PAGEOUT do not surface filesystem or
+ *     device I/O errors: vfs_fadvise() returns are discarded by
+ *     madvise_willneed() and the pageout walk is invoked through a void
+ *     helper, so transient I/O failures during read-ahead or page-out are
+ *     silently dropped.
+ *
+ * error: EBADF, Bad file descriptor
+ *   desc: Returned by MADV_WILLNEED when applied to a non-file-backed VMA
+ *     and the kernel was built without CONFIG_SWAP, so there is neither a
+ *     file to read-ahead from nor a swap device to fault from.
+ *
+ * error: EACCES, Permission denied
+ *   desc: Returned by MADV_REMOVE when the target VMA fails
+ *     vma_is_shared_maywrite(), which needs both VM_SHARED and VM_MAYWRITE.
+ *     Private file mappings and shared mappings of files not opened for
+ *     writing are refused, while a PROT_READ MAP_SHARED mapping of a file
+ *     opened O_RDWR is accepted. Punching a hole through a refused mapping
+ *     would either be invisible to other mappers or bypass file write
+ *     permission.
+ *
+ * error: EPERM, Operation not permitted
+ *   desc: Returned in two situations. First, MADV_HWPOISON and
+ *     MADV_SOFT_OFFLINE require CAP_SYS_ADMIN; the inject-error handler
+ *     refuses non-privileged callers. Second, on 64-bit kernels, a discard
+ *     operation (MADV_FREE, MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_REMOVE,
+ *     MADV_DONTFORK, MADV_WIPEONFORK, MADV_GUARD_INSTALL) is refused on a
+ *     read-only anonymous VMA that has been sealed with mseal(2), to prevent
+ *     bypassing the seal by discarding mapped data.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: Returned when a fatal signal is delivered while the call is
+ *     waiting to acquire the mmap write lock for a VMA-flag-mutating
+ *     behavior (mmap_write_lock_killable() returns -EINTR), or when
+ *     MADV_POPULATE_READ/MADV_POPULATE_WRITE is interrupted while faulting
+ *     in pages (faultin_page_range() returns -EINTR). Only a fatal signal
+ *     interrupts these waits, so the task is being killed and user space
+ *     does not normally see the error.
+ *
+ * error: EHWPOISON, Memory page has hardware error
+ *   desc: MADV_POPULATE_READ or MADV_POPULATE_WRITE encountered a page that
+ *     has been marked as containing a hardware-detected memory error and
+ *     could not be faulted in. MADV_HWPOISON also returns it when
+ *     memory_failure() finds the page already poisoned.
+ *
+ * error: EFAULT, Bad address
+ *   desc: MADV_POPULATE_READ or MADV_POPULATE_WRITE attempted to fault in a
+ *     page whose mapping raised VM_FAULT_SIGBUS or VM_FAULT_SIGSEGV (for
+ *     example, a file-backed page beyond the end of the file).
+ *     MADV_HWPOISON and MADV_SOFT_OFFLINE return a get_user_pages_fast()
+ *     failure, typically -EFAULT for an unmapped address.
+ *
+ * error: EBUSY, Device or resource busy
+ *   desc: Returned by MADV_COLLAPSE via madvise_collapse_errno() in two
+ *     specific scan-failure modes: SCAN_CGROUP_CHARGE_FAIL (the new
+ *     hugepage cannot be charged to the memory cgroup) and
+ *     SCAN_EXCEED_NONE_PTE (too many absent PTEs in the candidate range
+ *     for a synchronous collapse). Other transient collapse failures are
+ *     reported as -EAGAIN; non-transient ones as -EINVAL. MADV_HWPOISON and
+ *     MADV_SOFT_OFFLINE also return -EBUSY when the page cannot be handled.
+ *
+ * error: EOPNOTSUPP, Operation not supported
+ *   desc: MADV_REMOVE propagates vfs_fallocate() errors verbatim, so a
+ *     filesystem without FALLOC_FL_PUNCH_HOLE support fails with -EOPNOTSUPP
+ *     (other propagated errors include -EPERM, -ETXTBSY and -ENOSPC).
+ *     MADV_SOFT_OFFLINE also returns it when soft offlining is disabled via
+ *     /proc/sys/vm/enable_soft_offline or the event is filtered by
+ *     hwpoison_filter(); MADV_HWPOISON folds that case to 0.
+ *
+ * lock: mm->mmap_lock (read mode)
+ *   type: semaphore
+ *   acquired: yes
+ *   released: yes
+ *   desc: Taken for read for MADV_REMOVE, MADV_WILLNEED, MADV_COLD,
+ *     MADV_PAGEOUT, MADV_COLLAPSE, MADV_POPULATE_READ and
+ *     MADV_POPULATE_WRITE, and as the fallback when the per-VMA lock path
+ *     declines. It is dropped and retaken around vfs_fadvise() (WILLNEED on
+ *     regular files), vfs_fallocate() and userfaultfd_remove() (REMOVE, and
+ *     DONTNEED or FREE with UFFD_FEATURE_EVENT_REMOVE), the anon and file
+ *     collapse paths (COLLAPSE) and inside faultin_page_range()
+ *     (POPULATE_*), so the VMA must be looked up again afterwards.
+ *
+ * lock: mm->mmap_lock (write mode; killable)
+ *   type: semaphore
+ *   acquired: yes
+ *   released: yes
+ *   desc: Acquired in killable write mode for behaviors that modify
+ *     vma->vm_flags or split/merge VMAs (MADV_NORMAL, MADV_RANDOM,
+ *     MADV_SEQUENTIAL, MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP,
+ *     MADV_WIPEONFORK, MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE,
+ *     MADV_HUGEPAGE, MADV_NOHUGEPAGE). If the acquisition is killed by a
+ *     fatal signal, the syscall returns -EINTR before any VMA is touched.
+ *
+ * lock: per-VMA read lock (vma->vm_refcnt)
+ *   type: custom
+ *   acquired: yes
+ *   released: yes
+ *   desc: Tried first for MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE,
+ *     MADV_GUARD_INSTALL and MADV_GUARD_REMOVE via lock_vma_under_rcu(). The
+ *     lock is a reference on the VMA (vm_refcnt), not an rwsem. The per-VMA
+ *     path is taken only when the requested range fits within a single VMA,
+ *     the target mm is the caller's mm, the VMA is not armed with
+ *     userfaultfd, and, for MADV_GUARD_INSTALL on an anonymous VMA, an
+ *     anon_vma is already attached. Otherwise the code falls back to the
+ *     mmap read lock above.
+ *
+ * lock: mmu_gather TLB batch
+ *   type: custom
+ *   acquired: yes
+ *   released: yes
+ *   desc: For MADV_DONTNEED, MADV_DONTNEED_LOCKED and MADV_FREE a single
+ *     tlb_gather_mmu() / tlb_finish_mmu() pair (madvise_init_tlb() and
+ *     madvise_finish_tlb()) wraps the whole syscall, batching TLB invalidation
+ *     across all VMAs in the range. MADV_COLD and MADV_PAGEOUT build a
+ *     short-lived gather inside the handler. MADV_GUARD_INSTALL builds a
+ *     transient gather via zap_vma_range() each time the retry loop clears
+ *     pre-existing pages, and none if the range is already empty.
+ *     MADV_GUARD_REMOVE never gathers.
+ *
+ * lock: mmu_notifier invalidate range
+ *   type: custom
+ *   acquired: yes
+ *   released: yes
+ *   desc: All zap-based paths -- MADV_DONTNEED, MADV_DONTNEED_LOCKED, the
+ *     zap branch of MADV_GUARD_INSTALL via zap_vma_range(), and
+ *     MADV_FREE's own walk -- bracket their work with
+ *     mmu_notifier_invalidate_range_start()/_end() so secondary MMUs (KVM,
+ *     IOMMU SVA, etc.) observe the page clearing.
+ *
+ * signal: Any fatal signal
+ *   direction: receive
+ *   action: return
+ *   condition: Acquiring the mmap write lock or faulting in pages for
+ *     MADV_POPULATE_*
+ *   desc: A pending fatal signal aborts mmap_write_lock_killable() (used by
+ *     the VMA-flag-mutating behaviors) and faultin_page_range() (used by
+ *     MADV_POPULATE_READ and MADV_POPULATE_WRITE), in both cases surfacing as
+ *     -EINTR. Only a fatal signal interrupts these waits, so the task is
+ *     being killed and user space does not normally see the error.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: no
+ *
+ * side-effect: modify_state
+ *   target: vma->vm_flags
+ *   condition: Hint-group behaviors (MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL,
+ *     MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP, MADV_WIPEONFORK,
+ *     MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE, MADV_HUGEPAGE,
+ *     MADV_NOHUGEPAGE)
+ *   desc: Sets or clears VM_RAND_READ, VM_SEQ_READ, VM_DONTCOPY,
+ *     VM_DONTDUMP, VM_WIPEONFORK, VM_MERGEABLE, VM_HUGEPAGE or VM_NOHUGEPAGE
+ *     on the affected VMAs, splitting or merging VMAs as needed. Reversible
+ *     with the inverse advice (MADV_DOFORK undoes MADV_DONTFORK), except
+ *     that the inverse call's VMA filter still applies (DOFORK rejects
+ *     VM_SPECIAL, DODUMP rejects non-hugetlb VM_SPECIAL or VM_DROPPABLE,
+ *     KEEPONFORK rejects VM_DROPPABLE).
+ *   reversible: yes
+ *
+ * side-effect: free_memory | modify_state | irreversible
+ *   target: page tables and resident pages within the range
+ *   condition: MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE
+ *   desc: MADV_DONTNEED zaps PTEs, releasing the pages or swap slots so the
+ *     next access faults in zero-filled anonymous pages or re-reads the
+ *     file. MADV_DONTNEED_LOCKED is identical but tolerates VM_LOCKED.
+ *     MADV_FREE marks anonymous pages lazy-freeable; clean pages may be
+ *     reclaimed under memory pressure and a write before reclaim cancels
+ *     the lazy free. Discarded data cannot be recovered.
+ *   reversible: no
+ *
+ * side-effect: filesystem | irreversible
+ *   target: backing file (FALLOC_FL_PUNCH_HOLE)
+ *   condition: MADV_REMOVE
+ *   desc: Calls vfs_fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE) on
+ *     the backing file, deallocating the corresponding file blocks. The hole
+ *     is visible to all mappers of the file and to read(2)/write(2)
+ *     callers; subsequent reads return zero. Filesystem freeze protection,
+ *     i_rwsem and any quota/space accounting are taken by the underlying
+ *     fallocate path.
+ *   reversible: no
+ *
+ * side-effect: modify_state | schedule
+ *   target: LRU lists and page reclaim
+ *   condition: MADV_COLD, MADV_PAGEOUT
+ *   desc: MADV_COLD deactivates the affected pages, moving them to the
+ *     inactive LRU and clearing PG_referenced/PG_young so they are reclaimed
+ *     sooner under pressure. MADV_PAGEOUT additionally calls reclaim_pages()
+ *     to write dirty pages out and drop clean ones synchronously. Page data
+ *     is preserved (rereads will fault in the same content), but the I/O and
+ *     LRU bookkeeping cannot be undone.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: page tables (faultin)
+ *   condition: MADV_POPULATE_READ, MADV_POPULATE_WRITE
+ *   desc: Walks the requested range with faultin_page_range(), populating
+ *     PTEs by triggering read or write faults so subsequent accesses do not
+ *     fault. Equivalent to touching every page in the range while suppressing
+ *     SIGBUS/SIGSEGV through the syscall return value. Allocations made by
+ *     faultin are not undone on partial failure.
+ *   reversible: no
+ *
+ * side-effect: modify_state | schedule
+ *   target: transparent hugepage layout
+ *   condition: MADV_COLLAPSE
+ *   desc: Synchronously coalesces base pages in the range into a PMD-sized
+ *     transparent hugepage when the mapping permits. Performs the same page
+ *     migration and zeroing that khugepaged would do asynchronously; the
+ *     range's data is preserved across the collapse.
+ *   reversible: no
+ *
+ * side-effect: free_memory | modify_state | irreversible
+ *   target: PTE marker (PTE_MARKER_GUARD)
+ *   condition: MADV_GUARD_INSTALL, MADV_GUARD_REMOVE
+ *   desc: MADV_GUARD_INSTALL installs PTE_MARKER_GUARD entries that cause
+ *     subsequent accesses to deliver SIGSEGV without consuming physical
+ *     memory; existing pages already mapped in the range are zapped via
+ *     zap_vma_range() before the markers are installed, so any
+ *     prior contents are lost. MADV_GUARD_REMOVE clears the markers but
+ *     does not (and cannot) restore zapped data.
+ *   reversible: no
+ *
+ * side-effect: hardware | irreversible
+ *   target: physical page (memory_failure)
+ *   condition: MADV_HWPOISON
+ *   desc: MADV_HWPOISON marks the affected pages as containing an
+ *     unrecoverable hardware error using the same machine-check path that
+ *     real ECC failures take. This affects physical memory bookkeeping
+ *     kernel-wide, and madvise() has no inverse. The poison can be cleared
+ *     through unpoison_memory() (the hwpoison-inject debugfs interface).
+ *     Intended for testing the memory-failure pipeline; restricted to
+ *     CAP_SYS_ADMIN.
+ *   reversible: no
+ *
+ * side-effect: hardware
+ *   target: physical page (soft_offline_page)
+ *   condition: MADV_SOFT_OFFLINE
+ *   desc: Migrates the contents off the affected pages and removes them from
+ *     the buddy allocator. Page contents are preserved. madvise() has no
+ *     inverse. The pages can be brought back through unpoison_memory() (the
+ *     hwpoison-inject debugfs interface). Intended for testing the
+ *     memory-failure pipeline; restricted to CAP_SYS_ADMIN.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: KSM merge state (vm_flags & VM_MERGEABLE)
+ *   condition: MADV_MERGEABLE, MADV_UNMERGEABLE
+ *   desc: Toggles VM_MERGEABLE, the VMA's eligibility for the kernel
+ *     same-page merger. MADV_MERGEABLE lets ksmd later replace identical
+ *     anonymous pages with shared, write-protected copies and is silently
+ *     ignored on KSM-incompatible VMAs. MADV_UNMERGEABLE synchronously
+ *     calls break_ksm(), which faults each KSM page in the range back to an
+ *     exclusive copy, before clearing VM_MERGEABLE. An -ENOMEM from either
+ *     advice is returned as -EAGAIN. The flag toggle is reversible by
+ *     issuing the inverse advice.
+ *   reversible: yes
+ *
+ * side-effect: modify_state
+ *   target: userfaultfd event queue
+ *   condition: MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE, MADV_REMOVE
+ *     on a VMA whose userfaultfd context negotiated UFFD_FEATURE_EVENT_REMOVE
+ *   desc: Generates a UFFD_EVENT_REMOVE notification covering the discarded
+ *     range so userfaultfd monitors observing the mapping see the
+ *     invalidation. The event is queued before the discard takes effect; the
+ *     monitor cannot veto it.
+ *   reversible: no
+ *
+ * capability: CAP_SYS_ADMIN
+ *   type: perform_operation
+ *   allows: Inject memory errors via MADV_HWPOISON or MADV_SOFT_OFFLINE
+ *   without: Both behaviors return -EPERM
+ *   condition: Checked at entry to madvise_inject_error() before any pages
+ *     are looked up
+ *
+ * constraint: Page-aligned start
+ *   desc: start must lie on a page boundary; otherwise the call returns
+ *     -EINVAL before any VMA is consulted.
+ *   expr: (start & (PAGE_SIZE - 1)) == 0
+ *
+ * constraint: Length rounded up to PAGE_SIZE
+ *   desc: The effective range length is PAGE_ALIGN(len_in). A non-zero len_in
+ *     that overflows during rounding, or a (start, end) range that wraps,
+ *     is rejected with -EINVAL.
+ *   expr: end = start + PAGE_ALIGN(len_in); end >= start
+ *
+ * constraint: Behavior must be supported
+ *   desc: behavior must be one of the MADV_* values listed under the
+ *     behavior parameter. Behaviors gated by Kconfig (KSM, THP, memory
+ *     failure) are rejected with -EINVAL when the corresponding option is
+ *     disabled in the running kernel.
+ *
+ * constraint: mseal-protected discards
+ *   desc: On 64-bit kernels, a discard operation (FREE, DONTNEED,
+ *     DONTNEED_LOCKED, REMOVE, DONTFORK, WIPEONFORK, GUARD_INSTALL) against
+ *     a sealed anonymous VMA is rejected unless the mapping is currently
+ *     writable -- both VM_WRITE in vm_flags and arch_vma_access_permitted()
+ *     allowing write -- so that mseal(2) cannot be bypassed by instructing
+ *     the kernel to throw the data away. File-backed sealed VMAs and
+ *     writable sealed VMAs are not subject to this restriction.
+ *   expr: !is_discard(behavior) || !vma_is_sealed(vma) ||
+ *     !vma_is_anonymous(vma) || ((vma->vm_flags & VM_WRITE) &&
+ *     arch_vma_access_permitted(vma, true, false, false))
+ *
+ * constraint: MADV_FREE requires anonymous mappings
+ *   desc: MADV_FREE is defined only over anonymous mappings; the handler
+ *     requires vma_is_anonymous() (no vm_ops) and rejects file-backed VMAs,
+ *     including shared anonymous (shmem) VMAs, with -EINVAL.
+ *   expr: vma_is_anonymous(vma)
+ *
+ * constraint: MADV_WIPEONFORK requires private anonymous mappings
+ *   desc: MADV_WIPEONFORK rejects file-backed mappings and shared anonymous
+ *     mappings; only MAP_PRIVATE anonymous VMAs accept it. Both rejections
+ *     surface as -EINVAL.
+ *   expr: !vma->vm_file && !(vma->vm_flags & VM_SHARED)
+ *
+ * constraint: MADV_REMOVE requires a shared file mapping that may be written
+ *   desc: MADV_REMOVE rejects VM_LOCKED VMAs and VMAs without an associated
+ *     file/mapping/host inode with -EINVAL. It rejects VMAs failing
+ *     vma_is_shared_maywrite() (VM_SHARED and VM_MAYWRITE) with -EACCES,
+ *     which covers private file mappings and shared mappings of files not
+ *     opened for writing, but not a PROT_READ MAP_SHARED mapping of a file
+ *     opened O_RDWR.
+ *   expr: !(vma->vm_flags & VM_LOCKED) && vma->vm_file &&
+ *     vma->vm_file->f_mapping && vma->vm_file->f_mapping->host &&
+ *     vma_is_shared_maywrite(vma)
+ *
+ * constraint: MADV_COLD / MADV_PAGEOUT VMA filter
+ *   desc: Both behaviors require LRU-managed pages; they reject VMAs that
+ *     are mlocked, raw-PFN or hugetlb.
+ *   expr: !(vma->vm_flags & (VM_LOCKED | VM_PFNMAP | VM_HUGETLB))
+ *
+ * examples: madvise(p, len, MADV_SEQUENTIAL);  // set VM_SEQ_READ on the VMA
+ *   madvise(p, len, MADV_POPULATE_WRITE);  // prefault writable PTEs
+ *   madvise(p, len, MADV_DONTNEED);        // discard anonymous pages
+ *   madvise(p, len, MADV_GUARD_INSTALL);   // install SIGSEGV guard pages
+ *
+ * notes: Behavior introduction history (mainline): MADV_FREE in 4.5,
+ *   MADV_WIPEONFORK / MADV_KEEPONFORK in 4.14, MADV_COLD / MADV_PAGEOUT in
+ *   5.4, MADV_POPULATE_READ / MADV_POPULATE_WRITE in 5.14,
+ *   MADV_DONTNEED_LOCKED in 5.18, MADV_COLLAPSE in 6.1, MADV_GUARD_INSTALL /
+ *   MADV_GUARD_REMOVE in 6.13. Code that wants to remain portable to older
+ *   kernels must handle -EINVAL gracefully and fall back.
+ *
+ *   process_madvise(2) applies the same advice values to another process
+ *   identified by a pidfd. When the target mm is the caller's own (the
+ *   pidfd refers to the caller), any locally-supported MADV_* value is
+ *   accepted. When the target is a different mm, the behavior must be in
+ *   the non-destructive remote subset (MADV_COLD, MADV_PAGEOUT,
+ *   MADV_WILLNEED, MADV_COLLAPSE) or the call returns -EINVAL, and the
+ *   caller must hold CAP_SYS_NICE.
+ *
+ *   MADV_PAGEOUT on a non-anonymous VM_MAYSHARE mapping is a silent no-op
+ *   returning 0 unless can_do_file_pageout() holds, meaning the caller owns
+ *   the file or is capable over it (file_owner_or_capable(), which honors
+ *   the mount idmap) or may write it (file_permission(MAY_WRITE)). On a
+ *   private file mapping that fails the same test, only anonymous pages are
+ *   paged out. MADV_COLD has no such filter.
+ *
+ *   MADV_GUARD_INSTALL retries up to MAX_MADVISE_GUARD_RETRIES (3) times
+ *   when it loses races with concurrent faulting or khugepaged. If those
+ *   retries are exhausted the handler returns -ERESTARTNOINTR via
+ *   restart_syscall(), which sets TIF_SIGPENDING so the return path runs
+ *   signal handling. madvise() is then transparently re-executed with the
+ *   same arguments, after any handler for a pending signal has run, unless
+ *   a fatal signal terminates the task first. The caller never observes an
+ *   errno from the restart itself and the call appears to make eventual
+ *   forward progress.
+ *
+ *   anon_vma_prepare() failures inside MADV_GUARD_INSTALL bypass the
+ *   ENOMEM-to-EAGAIN translation that applies to the VMA-flag-mutating
+ *   behaviors and surface as -ENOMEM directly.
+ *
+ *   Architecture note: alpha defines MADV_DONTNEED as 6 (not 4) and reserves
+ *   MADV_SPACEAVAIL=5; portable code must use the symbolic names from
+ *   <sys/mman.h>.
+ */
 SYSCALL_DEFINE3(madvise, unsigned long, start, size_t, len_in, int, behavior)
 {
 	return do_madvise(current->mm, start, len_in, behavior);
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/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
+```
diff --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"));
+    }
+}
diff --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(&section);
+                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(&param_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(&section);
+                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(&section);
+                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(&["", "  "]), "");
+    }
+}
diff --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(())
+}
diff --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(&current_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(
+                                &current_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");
+    }
+}
diff --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));
+    }
+}
diff --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) = &param.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) = &param.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(())
+}
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()
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 273853937c254..d5f973eeefc83 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -60,6 +60,7 @@ TARGETS += intel_pstate
 TARGETS += iommu
 TARGETS += ipc
 TARGETS += ir
+TARGETS += kapi
 TARGETS += kcmp
 TARGETS += kexec
 TARGETS += kselftest_harness
diff --git a/tools/testing/selftests/kapi/.gitignore b/tools/testing/selftests/kapi/.gitignore
new file mode 100644
index 0000000000000..e60893d183d81
--- /dev/null
+++ b/tools/testing/selftests/kapi/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+test_kapi
diff --git a/tools/testing/selftests/kapi/Makefile b/tools/testing/selftests/kapi/Makefile
new file mode 100644
index 0000000000000..d1bb26541c0b8
--- /dev/null
+++ b/tools/testing/selftests/kapi/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_GEN_PROGS := test_kapi
+
+CFLAGS += -static -Wall -Wextra -O2 $(KHDR_INCLUDES)
+
+include ../lib.mk
diff --git a/tools/testing/selftests/kapi/kapi_test_util.h b/tools/testing/selftests/kapi/kapi_test_util.h
new file mode 100644
index 0000000000000..a09871207bfc6
--- /dev/null
+++ b/tools/testing/selftests/kapi/kapi_test_util.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Compatibility helpers for KAPI selftests.
+ *
+ * __NR_open does not exist on architectures using the generic syscall table
+ * (e.g. arm64, riscv); only __NR_openat does. There the wrapper falls back to
+ * openat(AT_FDCWD, ...), which does not go through sys_open, so the sys_open
+ * KAPI checks are not exercised.
+ */
+#ifndef KAPI_TEST_UTIL_H
+#define KAPI_TEST_UTIL_H
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/syscall.h>
+
+#ifndef __NR_open
+static inline long kapi_sys_open(const char *pathname, int flags, int mode)
+{
+	return syscall(__NR_openat, AT_FDCWD, pathname, flags, mode);
+}
+#else
+static inline long kapi_sys_open(const char *pathname, int flags, int mode)
+{
+	return syscall(__NR_open, pathname, flags, mode);
+}
+#endif
+
+#endif /* KAPI_TEST_UTIL_H */
diff --git a/tools/testing/selftests/kapi/test_kapi.c b/tools/testing/selftests/kapi/test_kapi.c
new file mode 100644
index 0000000000000..deccb075ee1f9
--- /dev/null
+++ b/tools/testing/selftests/kapi/test_kapi.c
@@ -0,0 +1,1163 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Userspace selftest for KAPI runtime verification of syscall parameters.
+ *
+ * Exercises sys_open, sys_read, sys_write, and sys_close through raw
+ * syscall() to ensure KAPI pre-validation wrappers interact correctly
+ * with normal kernel error handling.
+ *
+ * Requires CONFIG_KAPI_RUNTIME_CHECKS=y for full coverage; many tests
+ * also pass without it.
+ *
+ * TAP output format.
+ */
+
+#define _GNU_SOURCE
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <signal.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <linux/limits.h>
+#include "../kselftest.h"
+#include "kapi_test_util.h"
+
+#define NUM_TESTS 31
+
+/* Set from the SIGPIPE handler. */
+static volatile sig_atomic_t got_sigpipe;
+
+static void tap_ok(const char *desc)
+{
+	ksft_test_result_pass("%s\n", desc);
+}
+
+static void tap_fail(const char *desc, const char *reason)
+{
+	ksft_test_result_fail("%s: %s\n", desc, reason);
+}
+
+static void tap_skip(const char *desc, const char *reason)
+{
+	ksft_test_result_skip("%s: %s\n", desc, reason);
+}
+
+/*
+ * Return true when CONFIG_KAPI_RUNTIME_CHECKS is active: it rejects unknown
+ * open flag bits with EINVAL, while the kernel otherwise ignores them.
+ */
+static bool kapi_runtime_checks_active(void)
+{
+	long ret;
+
+	errno = 0;
+	ret = kapi_sys_open("/dev/null", 0x10000000, 0);
+	if (ret >= 0) {
+		syscall(__NR_close, (int)ret);
+		return false;
+	}
+
+	return errno == EINVAL;
+}
+
+static void sigpipe_handler(int sig)
+{
+	(void)sig;
+	got_sigpipe = 1;
+}
+
+/* ---- Valid operation tests ---- */
+
+/*
+ * Test 1: open a readable file
+ * Returns fd on success.
+ */
+static int test_open_valid(void)
+{
+	errno = 0;
+	long fd = kapi_sys_open("/etc/hostname", O_RDONLY, 0);
+
+	if (fd >= 0) {
+		tap_ok("open valid file");
+	} else {
+		/* /etc/hostname might not exist; try /etc/passwd */
+		errno = 0;
+		fd = kapi_sys_open("/etc/passwd", O_RDONLY, 0);
+		if (fd >= 0)
+			tap_ok("open valid file (fallback /etc/passwd)");
+		else
+			tap_fail("open valid file", strerror(errno));
+	}
+	return (int)fd;
+}
+
+/*
+ * Test 2: read from fd
+ */
+static void test_read_valid(int fd)
+{
+	char buf[256];
+
+	errno = 0;
+	long ret = syscall(__NR_read, fd, buf, sizeof(buf));
+
+	if (ret > 0)
+		tap_ok("read from valid fd");
+	else if (ret == 0)
+		tap_ok("read from valid fd (EOF)");
+	else
+		tap_fail("read from valid fd", strerror(errno));
+}
+
+/*
+ * Test 3: write to /dev/null
+ */
+static void test_write_valid(void)
+{
+	errno = 0;
+	long devnull = kapi_sys_open("/dev/null", O_WRONLY, 0);
+
+	if (devnull < 0) {
+		tap_fail("write to /dev/null (open failed)", strerror(errno));
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)devnull, "hello", 5);
+
+	if (ret == 5)
+		tap_ok("write to /dev/null");
+	else
+		tap_fail("write to /dev/null",
+			 ret < 0 ? strerror(errno) : "short write");
+
+	syscall(__NR_close, (int)devnull);
+}
+
+/*
+ * Test 4: close fd
+ */
+static void test_close_valid(int fd)
+{
+	errno = 0;
+	long ret = syscall(__NR_close, fd);
+
+	if (ret == 0)
+		tap_ok("close valid fd");
+	else
+		tap_fail("close valid fd", strerror(errno));
+}
+
+/* ---- KAPI parameter rejection tests ---- */
+
+/*
+ * Test 5: open with invalid flag bits
+ * 0x10000000 is outside the valid O_* mask, KAPI should reject.
+ */
+static void test_open_invalid_flags(void)
+{
+	long ret;
+
+	if (!kapi_runtime_checks_active()) {
+		tap_skip("open with invalid flags",
+			 "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+		return;
+	}
+
+	errno = 0;
+	/*
+	 * Use /dev/null (always present on any sane rootfs) so KAPI's flag
+	 * validation is reached before a path-lookup ENOENT can mask it.
+	 * 0x10000000 is outside the valid O_* mask.
+	 */
+	ret = kapi_sys_open("/dev/null", 0x10000000, 0);
+
+	if (ret == -1 && errno == EINVAL) {
+		tap_ok("open with invalid flags returns EINVAL");
+	} else if (ret >= 0) {
+		tap_fail("open with invalid flags", "expected EINVAL, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+			 strerror(errno));
+		tap_fail("open with invalid flags", msg);
+	}
+}
+
+/*
+ * Test 6: open with mode bits above S_IALLUGO
+ * The kernel masks mode with S_IALLUGO (07777), so KAPI must accept it.
+ */
+static void test_open_high_mode_bits(void)
+{
+	const char *desc = "open with mode bits above 07777 creates file";
+	char path[64];
+	struct stat st;
+	mode_t um, want;
+	long fd;
+
+	snprintf(path, sizeof(path), "/tmp/kapi_test_mode.%d", getpid());
+	unlink(path);
+
+	um = umask(0);
+	umask(um);
+	want = 07777 & ~um;
+
+	errno = 0;
+	fd = kapi_sys_open(path, O_CREAT | O_WRONLY | O_EXCL, 0xFFFF);
+	if (fd < 0) {
+		tap_fail(desc, strerror(errno));
+		return;
+	}
+
+	if (fstat((int)fd, &st) < 0) {
+		tap_fail(desc, strerror(errno));
+	} else if ((st.st_mode & 07777) != want) {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected mode %04o, got %04o",
+			 (unsigned int)want, (unsigned int)(st.st_mode & 07777));
+		tap_fail(desc, msg);
+	} else {
+		tap_ok(desc);
+	}
+
+	syscall(__NR_close, (int)fd);
+	unlink(path);
+}
+
+/*
+ * Test 7: open with NULL path
+ * KAPI USER_PATH constraint should reject NULL.
+ */
+static void test_open_null_path(void)
+{
+	errno = 0;
+	long ret = kapi_sys_open(NULL, O_RDONLY, 0);
+
+	if (ret == -1 && errno == EINVAL) {
+		tap_ok("open with NULL path returns EINVAL");
+	} else if (ret == -1 && errno == EFAULT) {
+		/* Kernel may catch this as EFAULT before KAPI */
+		tap_ok("open with NULL path returns EFAULT (acceptable)");
+	} else if (ret >= 0) {
+		tap_fail("open with NULL path", "expected error, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "got %s", strerror(errno));
+		tap_fail("open with NULL path", msg);
+	}
+}
+
+/*
+ * Test 8: open with flag bit 30 set (0x40000000)
+ * This bit is outside the valid O_* mask, KAPI should reject with EINVAL.
+ */
+static void test_open_flag_bit30(void)
+{
+	long ret;
+
+	if (!kapi_runtime_checks_active()) {
+		tap_skip("open with flag bit 30 (0x40000000) returns EINVAL",
+			 "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+		return;
+	}
+
+	errno = 0;
+	ret = kapi_sys_open("/dev/null", 0x40000000, 0);
+
+	if (ret == -1 && errno == EINVAL) {
+		tap_ok("open with flag bit 30 (0x40000000) returns EINVAL");
+	} else if (ret >= 0) {
+		tap_fail("open with flag bit 30 (0x40000000) returns EINVAL",
+			 "expected EINVAL, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+			 strerror(errno));
+		tap_fail("open with flag bit 30 (0x40000000) returns EINVAL",
+			 msg);
+	}
+}
+
+/* ---- Boundary condition and error path tests ---- */
+
+/*
+ * Test 9: read with fd=-1 should return an error.
+ * With CONFIG_KAPI_RUNTIME_CHECKS=y, KAPI validates the fd first and
+ * rejects negative fds (other than AT_FDCWD) with EINVAL.  Without
+ * KAPI, the kernel returns EBADF.  Accept either.
+ */
+static void test_read_bad_fd(void)
+{
+	char buf[16];
+
+	errno = 0;
+	long ret = syscall(__NR_read, -1, buf, sizeof(buf));
+
+	if (ret == -1 && (errno == EBADF || errno == EINVAL)) {
+		tap_ok("read with fd=-1 returns error");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF/EINVAL, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("read with fd=-1 returns error", msg);
+	}
+}
+
+/*
+ * Test 10: read with count=0 should return 0
+ */
+static void test_read_zero_count(void)
+{
+	char buf[1];
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("read with count=0 returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_read, (int)fd, buf, 0);
+
+	if (ret == 0) {
+		tap_ok("read with count=0 returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("read with count=0 returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 11: write with count=0 should return 0
+ */
+static void test_write_zero_count(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("write with count=0 returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, "x", 0);
+
+	if (ret == 0) {
+		tap_ok("write with count=0 returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("write with count=0 returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 12: open with a path longer than PATH_MAX should fail
+ * Expect ENAMETOOLONG or EINVAL.
+ */
+static void test_open_long_path(void)
+{
+	char *longpath;
+	size_t len = PATH_MAX + 256;
+
+	longpath = malloc(len);
+	if (!longpath) {
+		tap_fail("open with path > PATH_MAX", "malloc failed");
+		return;
+	}
+
+	memset(longpath, 'A', len - 1);
+	longpath[0] = '/';
+	longpath[len - 1] = '\0';
+
+	errno = 0;
+	long ret = kapi_sys_open(longpath, O_RDONLY, 0);
+
+	if (ret == -1 && (errno == ENAMETOOLONG || errno == EINVAL)) {
+		tap_ok("open with path > PATH_MAX returns ENAMETOOLONG/EINVAL");
+	} else if (ret >= 0) {
+		tap_fail("open with path > PATH_MAX",
+			 "expected error, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg),
+			 "expected ENAMETOOLONG/EINVAL, got %s",
+			 strerror(errno));
+		tap_fail("open with path > PATH_MAX", msg);
+	}
+
+	free(longpath);
+}
+
+/*
+ * Test 13: read with unmapped user pointer should return EFAULT or EINVAL.
+ * Use a pipe with data so the kernel actually tries to copy to the buffer.
+ */
+static void test_read_unmapped_buf(void)
+{
+	int pipefd[2];
+
+	if (pipe(pipefd) < 0) {
+		tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+			 "pipe() failed");
+		return;
+	}
+
+	/* Write some data so read has something to copy */
+	if (write(pipefd[1], "hello", 5) != 5) {
+		tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+			 "pipe write failed");
+		close(pipefd[0]);
+		close(pipefd[1]);
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_read, pipefd[0], (void *)0xDEAD0000, 16);
+
+	if (ret == -1 && (errno == EFAULT || errno == EINVAL)) {
+		tap_ok("read with unmapped buffer returns EFAULT/EINVAL");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg),
+			 "expected EFAULT/EINVAL, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+			 msg);
+	}
+
+	close(pipefd[0]);
+	close(pipefd[1]);
+}
+
+/*
+ * Test 14: write with unmapped user pointer should return EFAULT or EINVAL.
+ * Use a pipe so the kernel actually tries to copy from the buffer.
+ */
+static void test_write_unmapped_buf(void)
+{
+	int pipefd[2];
+
+	if (pipe(pipefd) < 0) {
+		tap_fail("write with unmapped buffer returns EFAULT/EINVAL",
+			 "pipe() failed");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, pipefd[1], (void *)0xDEAD0000, 16);
+
+	if (ret == -1 && (errno == EFAULT || errno == EINVAL)) {
+		tap_ok("write with unmapped buffer returns EFAULT/EINVAL");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg),
+			 "expected EFAULT/EINVAL, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("write with unmapped buffer returns EFAULT/EINVAL",
+			 msg);
+	}
+
+	close(pipefd[0]);
+	close(pipefd[1]);
+}
+
+/*
+ * Test 15: close an already-closed fd should return EBADF
+ */
+static void test_close_already_closed(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("close already-closed fd returns EBADF",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	/* Close it once - should succeed */
+	syscall(__NR_close, (int)fd);
+
+	/* Close it again - should fail with EBADF */
+	errno = 0;
+	long ret = syscall(__NR_close, (int)fd);
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("close already-closed fd returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret == 0 ? "success" : strerror(errno));
+		tap_fail("close already-closed fd returns EBADF", msg);
+	}
+}
+
+/*
+ * Test 16: open /dev/null with O_RDONLY|O_CLOEXEC should succeed
+ */
+static void test_open_valid_cloexec(void)
+{
+	errno = 0;
+	long fd = kapi_sys_open("/dev/null", O_RDONLY | O_CLOEXEC, 0);
+
+	if (fd >= 0) {
+		tap_ok("open /dev/null with O_RDONLY|O_CLOEXEC succeeds");
+		syscall(__NR_close, (int)fd);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected success, got %s",
+			 strerror(errno));
+		tap_fail("open /dev/null with O_RDONLY|O_CLOEXEC succeeds",
+			 msg);
+	}
+}
+
+/*
+ * Test 17: write 0 bytes to /dev/null should return 0
+ */
+static void test_write_zero_devnull(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("write 0 bytes to /dev/null returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, "", 0);
+
+	if (ret == 0) {
+		tap_ok("write 0 bytes to /dev/null returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("write 0 bytes to /dev/null returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 18: read from a write-only fd should return EBADF
+ */
+static void test_read_writeonly_fd(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("read from write-only fd returns EBADF",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	char buf[16];
+
+	errno = 0;
+	long ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("read from write-only fd returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("read from write-only fd returns EBADF", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 19: write to a read-only fd should return EBADF
+ */
+static void test_write_readonly_fd(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("write to read-only fd returns EBADF",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, "hello", 5);
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("write to read-only fd returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("write to read-only fd returns EBADF", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 20: close fd 9999 (likely invalid) should return EBADF
+ */
+static void test_close_fd_9999(void)
+{
+	errno = 0;
+	long ret = syscall(__NR_close, 9999);
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("close fd 9999 returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret == 0 ? "success" : strerror(errno));
+		tap_fail("close fd 9999 returns EBADF", msg);
+	}
+}
+
+/*
+ * Test 21: read from pipe after write end is closed returns 0 (EOF)
+ */
+static void test_read_closed_pipe(void)
+{
+	int pipefd[2];
+
+	if (pipe(pipefd) < 0) {
+		tap_fail("read from closed pipe returns 0 (EOF)",
+			 "pipe() failed");
+		return;
+	}
+
+	/* Close write end */
+	close(pipefd[1]);
+
+	char buf[16];
+
+	errno = 0;
+	long ret = syscall(__NR_read, pipefd[0], buf, sizeof(buf));
+
+	if (ret == 0) {
+		tap_ok("read from closed pipe returns 0 (EOF)");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, ret < 0 ? strerror(errno) : "n/a");
+		tap_fail("read from closed pipe returns 0 (EOF)", msg);
+	}
+
+	close(pipefd[0]);
+}
+
+/*
+ * Test 22: write to pipe after read end is closed returns EPIPE + SIGPIPE
+ */
+static void test_write_closed_pipe(void)
+{
+	int pipefd[2];
+	struct sigaction sa, old_sa;
+
+	if (pipe(pipefd) < 0) {
+		tap_fail("write to closed pipe returns EPIPE + SIGPIPE",
+			 "pipe() failed");
+		return;
+	}
+
+	/* Install SIGPIPE handler */
+	memset(&sa, 0, sizeof(sa));
+	sa.sa_handler = sigpipe_handler;
+	sigemptyset(&sa.sa_mask);
+	sigaction(SIGPIPE, &sa, &old_sa);
+
+	got_sigpipe = 0;
+
+	/* Close read end */
+	close(pipefd[0]);
+
+	errno = 0;
+	long ret = syscall(__NR_write, pipefd[1], "hello", 5);
+
+	if (ret == -1 && errno == EPIPE && got_sigpipe) {
+		tap_ok("write to closed pipe returns EPIPE + SIGPIPE");
+	} else if (ret == -1 && errno == EPIPE) {
+		tap_ok("write to closed pipe returns EPIPE (SIGPIPE not caught)");
+	} else {
+		char msg[128];
+
+		snprintf(msg, sizeof(msg),
+			 "expected EPIPE, got %s (sigpipe=%d)",
+			 ret >= 0 ? "success" : strerror(errno),
+			 (int)got_sigpipe);
+		tap_fail("write to closed pipe returns EPIPE + SIGPIPE", msg);
+	}
+
+	/* Restore SIGPIPE handler */
+	sigaction(SIGPIPE, &old_sa, NULL);
+	close(pipefd[1]);
+}
+
+/*
+ * Test 23: open with O_DIRECTORY on a non-directory returns ENOTDIR
+ */
+static void test_open_directory_on_file(void)
+{
+	errno = 0;
+	long ret = kapi_sys_open("/dev/null", O_RDONLY | O_DIRECTORY, 0);
+
+	if (ret == -1 && errno == ENOTDIR) {
+		tap_ok("open O_DIRECTORY on non-directory returns ENOTDIR");
+	} else if (ret >= 0) {
+		tap_fail("open O_DIRECTORY on non-directory",
+			 "expected ENOTDIR, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected ENOTDIR, got %s",
+			 strerror(errno));
+		tap_fail("open O_DIRECTORY on non-directory", msg);
+	}
+}
+
+/*
+ * Test 24: open nonexistent file without O_CREAT returns ENOENT
+ */
+static void test_open_nonexistent(void)
+{
+	errno = 0;
+	long ret = kapi_sys_open("/tmp/kapi_nonexistent_file_12345",
+				 O_RDONLY, 0);
+
+	if (ret == -1 && errno == ENOENT) {
+		tap_ok("open nonexistent file without O_CREAT returns ENOENT");
+	} else if (ret >= 0) {
+		tap_fail("open nonexistent file",
+			 "expected ENOENT, got success (file exists?)");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected ENOENT, got %s",
+			 strerror(errno));
+		tap_fail("open nonexistent file", msg);
+	}
+}
+
+/*
+ * Test 25: close stdin (fd 0) should succeed
+ * We dup it first so we can restore it.
+ */
+static void test_close_stdin(void)
+{
+	int saved_stdin = dup(0);
+
+	if (saved_stdin < 0) {
+		tap_fail("close stdin succeeds", "cannot dup stdin");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_close, 0);
+
+	if (ret == 0) {
+		tap_ok("close stdin (fd 0) succeeds");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected success, got %s",
+			 strerror(errno));
+		tap_fail("close stdin (fd 0) succeeds", msg);
+	}
+
+	/* Restore stdin */
+	dup2(saved_stdin, 0);
+	close(saved_stdin);
+}
+
+/*
+ * Test 26: read after close returns EBADF
+ */
+static void test_read_after_close(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("read after close returns EBADF",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	syscall(__NR_close, (int)fd);
+
+	char buf[16];
+
+	errno = 0;
+	long ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("read after close returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("read after close returns EBADF", msg);
+	}
+}
+
+/*
+ * Test 27: write with large count
+ * Without KAPI: the kernel clamps count to MAX_RW_COUNT and succeeds.
+ * With KAPI: KAPI validates the buffer against the count and may
+ * return EFAULT/EINVAL since the buffer is smaller than count.
+ * Accept either success or EFAULT/EINVAL.
+ */
+static void test_write_large_count(void)
+{
+	long fd;
+	char buf[64] = "test data";
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("write with large count handled correctly",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, buf, (size_t)0x7ffff000UL);
+
+	if (ret > 0) {
+		tap_ok("write with large count succeeds (clamped, no KAPI)");
+	} else if (ret == -1 && (errno == EFAULT || errno == EINVAL)) {
+		tap_ok("write with large count returns EFAULT/EINVAL (KAPI validates buffer)");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected success or EFAULT, got %s",
+			 ret == 0 ? "zero" : strerror(errno));
+		tap_fail("write with large count handled correctly", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/* ---- Integration tests ---- */
+
+/*
+ * Test 28: full normal syscall path - open, read, write, close
+ * Verify KAPI does not interfere with normal operations.
+ */
+static void test_normal_path(void)
+{
+	long rd_fd, wr_fd;
+	char buf[128];
+	int ok = 1;
+	char reason[128] = "";
+
+	/* Open a readable file */
+	errno = 0;
+	rd_fd = kapi_sys_open("/etc/hostname", O_RDONLY, 0);
+	if (rd_fd < 0) {
+		errno = 0;
+		rd_fd = kapi_sys_open("/etc/passwd", O_RDONLY, 0);
+	}
+	if (rd_fd < 0) {
+		snprintf(reason, sizeof(reason), "open readable file: %s",
+			 strerror(errno));
+		ok = 0;
+	}
+
+	/* Read from it */
+	if (ok) {
+		errno = 0;
+		long n = syscall(__NR_read, (int)rd_fd, buf, sizeof(buf));
+
+		if (n < 0) {
+			snprintf(reason, sizeof(reason), "read: %s",
+				 strerror(errno));
+			ok = 0;
+		}
+	}
+
+	/* Open /dev/null for writing */
+	wr_fd = -1;
+	if (ok) {
+		errno = 0;
+		wr_fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+		if (wr_fd < 0) {
+			snprintf(reason, sizeof(reason),
+				 "open /dev/null: %s", strerror(errno));
+			ok = 0;
+		}
+	}
+
+	/* Write to /dev/null */
+	if (ok) {
+		errno = 0;
+		long n = syscall(__NR_write, (int)wr_fd, "test", 4);
+
+		if (n != 4) {
+			snprintf(reason, sizeof(reason), "write: %s",
+				 n < 0 ? strerror(errno) : "short write");
+			ok = 0;
+		}
+	}
+
+	/* Close both fds */
+	if (rd_fd >= 0) {
+		errno = 0;
+		if (syscall(__NR_close, (int)rd_fd) != 0 && ok) {
+			snprintf(reason, sizeof(reason), "close read fd: %s",
+				 strerror(errno));
+			ok = 0;
+		}
+	}
+
+	if (wr_fd >= 0) {
+		errno = 0;
+		if (syscall(__NR_close, (int)wr_fd) != 0 && ok) {
+			snprintf(reason, sizeof(reason), "close write fd: %s",
+				 strerror(errno));
+			ok = 0;
+		}
+	}
+
+	if (ok)
+		tap_ok("normal syscall path (open/read/write/close) works");
+	else
+		tap_fail("normal syscall path (open/read/write/close) works",
+			 reason);
+}
+
+/*
+ * Test 29: read with a NULL buffer and count=0 should return 0
+ * No access is performed, so KAPI must not reject the NULL pointer.
+ */
+static void test_read_null_zero_count(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("read with NULL buffer and count=0 returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_read, (int)fd, NULL, 0);
+
+	if (ret == 0) {
+		tap_ok("read with NULL buffer and count=0 returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("read with NULL buffer and count=0 returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 30: write with a NULL buffer and count=0 should return 0
+ * No access is performed, so KAPI must not reject the NULL pointer.
+ */
+static void test_write_null_zero_count(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("write with NULL buffer and count=0 returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, NULL, 0);
+
+	if (ret == 0) {
+		tap_ok("write with NULL buffer and count=0 returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("write with NULL buffer and count=0 returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 31: verify dmesg contains KAPI warnings for the invalid tests
+ */
+static void test_dmesg_warnings(void)
+{
+	int kmsg_fd;
+
+	if (!kapi_runtime_checks_active()) {
+		tap_skip("dmesg contains expected KAPI warnings",
+			 "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+		return;
+	}
+
+	kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+	if (kmsg_fd < 0) {
+		tap_skip("dmesg contains expected KAPI warnings",
+			 "cannot open /dev/kmsg");
+		return;
+	}
+
+	char line[4096];
+	int found_invalid_bits = 0;
+	int found_null = 0;
+	ssize_t n;
+
+	for (;;) {
+		n = read(kmsg_fd, line, sizeof(line) - 1);
+		if (n > 0) {
+			line[n] = '\0';
+			if (strstr(line, "contains invalid bits"))
+				found_invalid_bits++;
+			if (strstr(line, "NULL") && strstr(line, "not allowed"))
+				found_null++;
+		} else if (n == -1 && errno == EPIPE) {
+			/* Ring buffer wrapped, continue reading */
+			continue;
+		} else {
+			/* EAGAIN (no more messages) or other error */
+			break;
+		}
+	}
+
+	close(kmsg_fd);
+
+	if (found_invalid_bits >= 2 && found_null >= 1) {
+		tap_ok("dmesg contains expected KAPI warnings");
+	} else {
+		char msg[128];
+
+		snprintf(msg, sizeof(msg),
+			 "expected invalid_bits>=2 null>=1, got invalid_bits=%d null=%d",
+			 found_invalid_bits, found_null);
+		tap_fail("dmesg contains expected KAPI warnings", msg);
+	}
+}
+
+int main(void)
+{
+	ksft_print_header();
+	ksft_set_plan(NUM_TESTS);
+
+	/* Valid operations (1-4) */
+	int fd = test_open_valid();
+
+	if (fd >= 0)
+		test_read_valid(fd);
+	else
+		tap_fail("read from valid fd", "no fd from open");
+
+	test_write_valid();
+
+	if (fd >= 0)
+		test_close_valid(fd);
+	else
+		tap_fail("close valid fd", "no fd from open");
+
+	/* KAPI parameter validation (5-8) */
+	test_open_invalid_flags();
+	test_open_high_mode_bits();
+	test_open_null_path();
+	test_open_flag_bit30();
+
+	/* Boundary conditions and error paths (9-20) */
+	test_read_bad_fd();
+	test_read_zero_count();
+	test_write_zero_count();
+	test_open_long_path();
+	test_read_unmapped_buf();
+	test_write_unmapped_buf();
+	test_close_already_closed();
+	test_open_valid_cloexec();
+	test_write_zero_devnull();
+	test_read_writeonly_fd();
+	test_write_readonly_fd();
+	test_close_fd_9999();
+
+	/* Pipe and lifecycle tests (21-27) */
+	test_read_closed_pipe();
+	test_write_closed_pipe();
+	test_open_directory_on_file();
+	test_open_nonexistent();
+	test_close_stdin();
+	test_read_after_close();
+	test_write_large_count();
+
+	/* Integration (28-31) */
+	test_normal_path();
+	test_read_null_zero_count();
+	test_write_null_zero_count();
+	test_dmesg_warnings();
+
+	ksft_finished();
+	return 0;
+}

3/2 2026/10/08 12:29 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 251556
							output: 30
							thoughts: 2209
						

Thoughts:
**KAPI Patch Series Analysis**

Alright, let's break down this patch commit, 866c60e83179de8f813261f2eb9fcbe27010e491, on amd64.  My primary concern is whether this introduces code that's both *reachable* and potentially *fuzzable*, keeping in mind the core fuzzing philosophy. This seems to be introducing a "Kernel API Specification Framework" (KAPI).  Let's see if it’s worth fuzzing or if it can be dismissed.

First, I see several key additions: documentation, `MAINTAINERS` updates, and build system changes. Standard stuff. The inclusion of `.gitignore` for `*.apispec.h` is unremarkable.  But let's look at the core of the code.  The patch modifies system call handling via `arch/x86/include/asm/syscall_wrapper.h` and `include/linux/syscalls.h`.  Specifically, `__KAPI_SYSCALL_DEFINEx` is inserted, and a `CONFIG_KAPI_RUNTIME_CHECKS` option is added.  *This* is significant.

When `CONFIG_KAPI_RUNTIME_CHECKS` is enabled, every syscall defined with `SYSCALL_DEFINEx` (on x86 or the generic syscall wrapper) now executes code involving `kapi_get_spec`, `kapi_validate_syscall_params`, and `kapi_validate_syscall_return`.  This is a *hook* that is *reachable*.  Crucially, the code calls `kapi_get_spec` which takes the `api_spec_mutex`. This implies a potential contention and concurrency issue.

Furthermore, there is a debugfs interface (`kapi_debugfs.c`)!  We have debugfs entries `/sys/kernel/debug/kapi/list`, `/sys/kernel/debug/kapi/specs/*`, `/sys/kernel/debug/kapi/specs-json/*`!  And `kapi_export_json` is called when reading `specs-json/*`.  This is user-accessible code! We can reach this via debugfs!

Now, the crucial question: is this reachable code fuzzable?  The fuzzing philosophy emphasizes reachability as *the* primary gate.  Given that we are modifying the syscall handler, and exposing it through a debugfs interface, the new KAPI code *is* reachable. The debugfs code is a solid path to coverage.

The code in `arch/x86/include/asm/syscall_wrapper.h` and `include/linux/syscalls.h` is now modified to execute `kapi_get_spec` on *every* syscall entry when `CONFIG_KAPI_RUNTIME_CHECKS` is enabled.  And `kapi_get_spec` calls `mutex_lock(&api_spec_mutex)`. We should probably investigate the validation logic as well!  `read-file` or `grepper` will be used for code inspection.  The patch introduces new checks that will be executed!

The introduction of new code that can *actually* be reached via debugfs, and changes to syscall handling, is sufficient to classify this as WorthFuzzing=true.



4/2 2026/10/08 12:29 tool grepper
0m Args:
{
  "Expression": "kapi_validate_",
  "PathPrefixes": [
    "kernel/api/"
  ]
}

Results:
{
  "Output": "\nFull output is too long, showing 200 out of 306 lines.\nUse more precise expression or PathPrefixes if possible.\n\nNumber of matching lines per file (2 files in total):\nkernel/api/kapi_kunit.c:52\nkernel/api/kernel_api_spec.c:43\n\nkernel/api/kapi_kunit.c=148=static void test_constraint_range_valid(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-156-\nkernel/api/kapi_kunit.c:157:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0));\nkernel/api/kapi_kunit.c:158:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 50));\nkernel/api/kapi_kunit.c:159:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 100));\nkernel/api/kapi_kunit.c-160-\n--\nkernel/api/kapi_kunit.c-162-\tparam.max_value = -1;\nkernel/api/kapi_kunit.c:163:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, -10));\nkernel/api/kapi_kunit.c:164:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, -1));\nkernel/api/kapi_kunit.c-165-\n--\nkernel/api/kapi_kunit.c-168-\tparam.max_value = (s64)U64_MAX;\nkernel/api/kapi_kunit.c:169:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, S64_MAX));\nkernel/api/kapi_kunit.c-170-}\n--\nkernel/api/kapi_kunit.c=173=static void test_constraint_range_invalid(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-181-\nkernel/api/kapi_kunit.c:182:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, -1));\nkernel/api/kapi_kunit.c:183:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 101));\nkernel/api/kapi_kunit.c-184-\n--\nkernel/api/kapi_kunit.c-186-\tparam.max_value = -1;\nkernel/api/kapi_kunit.c:187:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, -11));\nkernel/api/kapi_kunit.c:188:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0));\nkernel/api/kapi_kunit.c:189:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 5));\nkernel/api/kapi_kunit.c-190-}\n--\nkernel/api/kapi_kunit.c=193=static void test_constraint_mask_valid(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-200-\nkernel/api/kapi_kunit.c:201:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0x00));\nkernel/api/kapi_kunit.c:202:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0x0F));\nkernel/api/kapi_kunit.c:203:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0xFF));\nkernel/api/kapi_kunit.c-204-}\n--\nkernel/api/kapi_kunit.c=207=static void test_constraint_mask_invalid(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-214-\nkernel/api/kapi_kunit.c:215:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0x100));\nkernel/api/kapi_kunit.c:216:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0x1FF));\nkernel/api/kapi_kunit.c-217-}\n--\nkernel/api/kapi_kunit.c=220=static void test_constraint_power_of_two(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-226-\nkernel/api/kapi_kunit.c:227:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 1));\nkernel/api/kapi_kunit.c:228:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 2));\nkernel/api/kapi_kunit.c:229:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 4));\nkernel/api/kapi_kunit.c:230:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 8));\nkernel/api/kapi_kunit.c:231:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0));\nkernel/api/kapi_kunit.c:232:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 3));\nkernel/api/kapi_kunit.c:233:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 5));\nkernel/api/kapi_kunit.c-234-}\n--\nkernel/api/kapi_kunit.c=237=static void test_constraint_page_aligned(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-243-\nkernel/api/kapi_kunit.c:244:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0));\nkernel/api/kapi_kunit.c:245:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, PAGE_SIZE));\nkernel/api/kapi_kunit.c:246:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 2 * PAGE_SIZE));\nkernel/api/kapi_kunit.c:247:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 1));\nkernel/api/kapi_kunit.c:248:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, PAGE_SIZE - 1));\nkernel/api/kapi_kunit.c-249-}\n--\nkernel/api/kapi_kunit.c=252=static void test_constraint_nonzero(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-258-\nkernel/api/kapi_kunit.c:259:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0));\nkernel/api/kapi_kunit.c:260:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 1));\nkernel/api/kapi_kunit.c:261:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, -1));\nkernel/api/kapi_kunit.c-262-}\n--\nkernel/api/kapi_kunit.c=265=static void test_return_validation(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-276-\nkernel/api/kapi_kunit.c:277:\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 0));\nkernel/api/kapi_kunit.c-278-}\n--\nkernel/api/kapi_kunit.c=281=static void test_return_known_error(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-295-\t/* -ENOENT is in the error list, so it's valid */\nkernel/api/kapi_kunit.c:296:\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, -ENOENT));\nkernel/api/kapi_kunit.c-297-}\n--\nkernel/api/kapi_kunit.c=300=static void test_return_unknown_error(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-316-\t */\nkernel/api/kapi_kunit.c:317:\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, -EPERM));\nkernel/api/kapi_kunit.c-318-}\n--\nkernel/api/kapi_kunit.c=321=static void test_constraint_alignment(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-328-\nkernel/api/kapi_kunit.c:329:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0));\nkernel/api/kapi_kunit.c:330:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 8));\nkernel/api/kapi_kunit.c:331:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 16));\nkernel/api/kapi_kunit.c:332:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 1));\nkernel/api/kapi_kunit.c:333:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 7));\nkernel/api/kapi_kunit.c-334-}\n--\nkernel/api/kapi_kunit.c=337=static void test_fd_int_overflow(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-345-\t/* Value that overflows int: 0x100000003 -\u003e truncates to 3 */\nkernel/api/kapi_kunit.c:346:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0x100000003LL));\nkernel/api/kapi_kunit.c-347-}\n--\nkernel/api/kapi_kunit.c=352=static void test_constraint_enum(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-360-\nkernel/api/kapi_kunit.c:361:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 1));\nkernel/api/kapi_kunit.c:362:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 5));\nkernel/api/kapi_kunit.c:363:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 10));\nkernel/api/kapi_kunit.c:364:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 0));\nkernel/api/kapi_kunit.c:365:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 3));\nkernel/api/kapi_kunit.c:366:\tKUNIT_EXPECT_FALSE(test, kapi_validate_param(\u0026param, 11));\nkernel/api/kapi_kunit.c-367-}\n--\nkernel/api/kapi_kunit.c=370=static void test_constraint_buffer(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-377-\t/* Buffer constraint doesn't validate the value itself */\nkernel/api/kapi_kunit.c:378:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 0));\nkernel/api/kapi_kunit.c:379:\tKUNIT_EXPECT_TRUE(test, kapi_validate_param(\u0026param, 4096));\nkernel/api/kapi_kunit.c-380-}\n--\nkernel/api/kapi_kunit.c=383=static void test_return_range(struct kunit *test)\n--\nkernel/api/kapi_kunit.c-395-\nkernel/api/kapi_kunit.c:396:\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 0));\nkernel/api/kapi_kunit.c:397:\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 50));\nkernel/api/kapi_kunit.c:398:\tKUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 100));\nkernel/api/kapi_kunit.c-399-}\n--\nkernel/api/kapi_kunit.c=409=static bool validate_dyn_buf(const struct kapi_param_spec *param,\n--\nkernel/api/kapi_kunit.c-413-\nkernel/api/kapi_kunit.c:414:\treturn kapi_validate_param_with_context(param, params[0], params,\nkernel/api/kapi_kunit.c-415-\t\t\t\t\t\tARRAY_SIZE(params));\n--\nkernel/api/kernel_api_spec.c=762=static void kapi_trace_format_params(const struct kernel_api_spec *spec,\n--\nkernel/api/kernel_api_spec.c-793-/**\nkernel/api/kernel_api_spec.c:794: * kapi_validate_fd - Validate that a file descriptor value is in valid range\nkernel/api/kernel_api_spec.c-795- * @fd: File descriptor to validate\n--\nkernel/api/kernel_api_spec.c-803- */\nkernel/api/kernel_api_spec.c:804:static bool kapi_validate_fd(int fd)\nkernel/api/kernel_api_spec.c-805-{\n--\nkernel/api/kernel_api_spec.c-809-/**\nkernel/api/kernel_api_spec.c:810: * kapi_validate_user_ptr - Validate that a user pointer is accessible\nkernel/api/kernel_api_spec.c-811- * @ptr: User pointer to validate\n--\nkernel/api/kernel_api_spec.c-815- */\nkernel/api/kernel_api_spec.c:816:static bool kapi_validate_user_ptr(const void __user *ptr, size_t size)\nkernel/api/kernel_api_spec.c-817-{\n--\nkernel/api/kernel_api_spec.c-825-/**\nkernel/api/kernel_api_spec.c:826: * kapi_validate_user_ptr_with_params - Validate user pointer with dynamic size\nkernel/api/kernel_api_spec.c-827- * @param_spec: Parameter specification\n--\nkernel/api/kernel_api_spec.c-833- */\nkernel/api/kernel_api_spec.c:834:static bool kapi_validate_user_ptr_with_params(const struct kapi_param_spec *param_spec,\nkernel/api/kernel_api_spec.c-835-\t\t\t\t\t\tconst void __user *ptr,\n--\nkernel/api/kernel_api_spec.c-874-\nkernel/api/kernel_api_spec.c:875:\treturn kapi_validate_user_ptr(ptr, actual_size);\nkernel/api/kernel_api_spec.c-876-}\n--\nkernel/api/kernel_api_spec.c-878-/**\nkernel/api/kernel_api_spec.c:879: * kapi_validate_path - Validate that a pathname is accessible and within limits\nkernel/api/kernel_api_spec.c-880- * @path: User pointer to pathname\n--\nkernel/api/kernel_api_spec.c-884- */\nkernel/api/kernel_api_spec.c:885:static bool kapi_validate_path(const char __user *path,\nkernel/api/kernel_api_spec.c-886-\t\t\t\tconst struct kapi_param_spec *param_spec)\n--\nkernel/api/kernel_api_spec.c-928-/**\nkernel/api/kernel_api_spec.c:929: * kapi_validate_user_string - Validate a userspace null-terminated string\nkernel/api/kernel_api_spec.c-930- * @str: User pointer to string\n--\nkernel/api/kernel_api_spec.c-938- */\nkernel/api/kernel_api_spec.c:939:static bool kapi_validate_user_string(const char __user *str,\nkernel/api/kernel_api_spec.c-940-\t\t\t\t       const struct kapi_param_spec *param_spec)\n--\nkernel/api/kernel_api_spec.c-965-\t * Note: strnlen_user() is subject to TOCTOU -- see comment in\nkernel/api/kernel_api_spec.c:966:\t * kapi_validate_path() above. This check is best-effort.\nkernel/api/kernel_api_spec.c-967-\t */\n--\nkernel/api/kernel_api_spec.c-999-/**\nkernel/api/kernel_api_spec.c:1000: * kapi_validate_user_ptr_constraint - Validate a userspace pointer with size\nkernel/api/kernel_api_spec.c-1001- * @ptr: User pointer to validate\n--\nkernel/api/kernel_api_spec.c-1009- */\nkernel/api/kernel_api_spec.c:1010:static bool kapi_validate_user_ptr_constraint(const void __user *ptr,\nkernel/api/kernel_api_spec.c-1011-\t\t\t\t\t       const struct kapi_param_spec *param_spec)\n--\nkernel/api/kernel_api_spec.c-1040- */\n\n"
}

5/2 2026/10/08 12:29 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 259233
							output: 30
							thoughts: 2066
						

Thoughts:
**KAPI Internals: A Deep Dive**

Okay, so I'm digging into how this `kapi` thing works, specifically looking at its compile-time and runtime behavior. First, I need to understand how `kapi_get_spec` gets called. I see it's defined within `include/linux/syscalls.h`. It looks like when `CONFIG_KAPI_RUNTIME_CHECKS` is enabled, every syscall defined via `__SYSCALL_DEFINEx` ends up running `__do_kapi_sys##name`. This, in turn, calls `kapi_get_spec`. I need to remember that this covers syscalls like `open`, `read`, `write`, `close`, `madvise`, all the standard ones.

But what happens if `CONFIG_KAPI_RUNTIME_CHECKS` is *not* enabled? I know I've seen `CONFIG_KAPI_SPEC` and `CONFIG_KAPI_SPEC_DEBUGFS`. If *those* are enabled, can userspace still get at the data? Ah, yes! Userspace can read `/sys/kernel/debug/kapi/list`, `/sys/kernel/debug/kapi/specs/*`, `/sys/kernel/debug/kapi/specs-json/*`. Those files are generated by `kapi_spec_json_show`, `kapi_spec_show`, and `kapi_list_show`, which makes perfect sense.

I need to nail down the dependencies: `KAPI_SPEC` is fundamental, and then `KAPI_RUNTIME_CHECKS` depends on it, but also on whether `DEBUG_KERNEL` is on, and also whether we're on x86, or if we have syscall wrappers. `KAPI_SPEC_DEBUGFS` depends on the others, and on `DEBUG_FS`. Good.

I'm noting that this patch touches key parts of the kernel. It modifies `arch/x86/include/asm/syscall_wrapper.h` (every x86 syscall wrapper!), `include/linux/syscalls.h` (core syscall macros), the core specification engine (`kernel_api_spec.c`), the debugfs interface (`kapi_debugfs.c`), the linker script (`vmlinux.lds.h`, for the `.kapi_specs` section), and even the build system (`scripts/Makefile.build`). That's a serious footprint.

Now, that build system… If `CONFIG_KAPI_SPEC` is on, it runs `kernel-doc -apispec` during the build. And I see that for files like `fs/open.c`, `fs/read_write.c`, and `mm/madvise.c`, it generates `.apispec.h` files which contain the `DEFINE_KERNEL_API_SPEC` macros, populating the `.kapi_specs` section!

So at runtime, if `CONFIG_KAPI_RUNTIME_CHECKS` is active, that's when things get interesting. When syscalls like `read`, `write`, `open`, `close`, and `madvise` are called, `__do_kapi_sys` intercepts them. This then leads to the crucial `kapi_get_spec("sys" #name)` call.

Wait, I need to be exact here. In the `include/linux/syscalls.h` code, `"sys" #name`... the `#` preprocessor operator... is that *concatenation*? `"sys" "read"` becomes `"sysread"`! Hmmm. But what name does the kernel docs use, and the spec definition itself? I need to check the exact `fs/read_write.c` code. Okay, there it is. The documentation uses `sys_read`, and the actual syscall is `SYSCALL_DEFINE3(read, ...)` . So, the spec *name* is the crucial thing! And I need to know what the KDoc tool actually generates... Back to the source... Ah, I need to check the `tools/lib/python/kdoc/kdoc_apispec.py` to understand the actual naming scheme.



6/2 2026/10/08 12:30 tool grepper
0m Args:
{
  "Expression": "DEFINE_KERNEL_API_SPEC",
  "PathPrefixes": [
    "tools/"
  ]
}

Results:
{
  "Output": "tools/lib/python/kdoc/kdoc_apispec.py=379=class ApiSpecFormat(OutputFormat):\n--\ntools/lib/python/kdoc/kdoc_apispec.py-1000-        # Start macro invocation\ntools/lib/python/kdoc/kdoc_apispec.py:1001:        self.data += f\"DEFINE_KERNEL_API_SPEC({function_name})\\n\"\ntools/lib/python/kdoc/kdoc_apispec.py-1002-\n"
}

7/2 2026/10/08 12:30 llm patch-evaluator
1m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 262360
							output: 44
							thoughts: 53
						

Thoughts:
Here's my analysis of what I'm thinking:

**Examining the `kdoc_apispec.py` Function**

Okay, so I need to understand what's happening within the `function_name` function. My mental map is directing me to `kdoc_apispec.py`. I'm going to navigate to that file and, based on the context, I suspect the core logic relevant to me is likely around line 1000 of the script. My focus will be on that section to find the definition or calls relevant to function_name. I'll read through that section and understand its mechanics.



8/2 2026/10/08 12:31 tool read-file
0m Args:
{
  "File": "tools/lib/python/kdoc/kdoc_apispec.py",
  "FirstLine": 985,
  "LineCount": 40
}

Results:
{
  "Contents": " 985:\t        return has_section(context_keys) and has_section(indicators)\n 986:\t\n 987:\t    def out_function(self, fname, name, args):\n 988:\t        \"\"\"Generate API spec for a function\"\"\"\n 989:\t        function_name = args.get('function', name)\n 990:\t        sections = args.sections if hasattr(args, 'sections') else args.get('sections', {})\n 991:\t\n 992:\t        if not self._has_api_spec(sections):\n 993:\t            return\n 994:\t\n 995:\t        parameterlist = args.parameterlist if hasattr(args, 'parameterlist') else args.get('parameterlist', [])\n 996:\t        parameterdescs = args.parameterdescs if hasattr(args, 'parameterdescs') else args.get('parameterdescs', {})\n 997:\t        parametertypes = args.parametertypes if hasattr(args, 'parametertypes') else args.get('parametertypes', {})\n 998:\t        purpose = args.get('purpose', '')\n 999:\t\n1000:\t        # Start macro invocation\n1001:\t        self.data += f\"DEFINE_KERNEL_API_SPEC({function_name})\\n\"\n1002:\t\n1003:\t        # Basic info\n1004:\t        if purpose:\n1005:\t            self.data += f\"\\tKAPI_DESCRIPTION({self._format_macro_param(purpose)})\\n\"\n1006:\t\n1007:\t        long_desc = self._get_multiline_section(sections, 'long-desc')\n1008:\t        if long_desc:\n1009:\t            self.data += f\"\\tKAPI_LONG_DESC({self._format_macro_param(long_desc, True)})\\n\"\n1010:\t\n1011:\t        # Context flags. `contexts:` and `context-flags:` both work;\n1012:\t        # tokens canonicalise to KAPI_CTX_* for KAPI_CONTEXT().\n1013:\t        context = (self._get_section(sections, 'contexts')\n1014:\t                   or self._get_section(sections, 'context-flags'))\n1015:\t        if context:\n1016:\t            self.data += f\"\\tKAPI_CONTEXT({_canon_context_expr(context)})\\n\"\n1017:\t\n1018:\t        # Process parameters\n1019:\t        self._process_parameters(sections, parameterlist, parameterdescs, parametertypes)\n1020:\t\n1021:\t        # Process return value\n1022:\t        self._process_return(sections)\n1023:\t\n1024:\t        # Process errors\n"
}

9/2 2026/10/08 12:31 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 263324
							output: 35
							thoughts: 84
						

Thoughts:
Okay, let's see. My mind's buzzing... I need to figure out how `kdoc` pulls that function signature, specifically `SYSCALL_DEFINE3`, from C code.

**Diving into `kdoc_parser.py`**

Right, I'm thinking I should start by digging into `kdoc_parser.py` itself. My immediate instinct is to see how it handles function definitions, and of course, how it's designed to specifically recognize and parse those syscall macros. I need to understand the parsing logic, especially where it handles function signatures. My familiarity with this style of kernel documentation demands I follow this line of thinking. It's likely using regular expressions, or perhaps a more sophisticated parsing library. I need to follow its processing steps carefully. Time to get down to details.



10/2 2026/10/08 12:31 tool grepper
0m Args:
{
  "Expression": "SYSCALL_DEFINE",
  "PathPrefixes": [
    "tools/lib/python/kdoc/"
  ]
}

Results:
{
  "Output": "tools/lib/python/kdoc/kdoc_parser.py=319=class KernelDoc:\n--\ntools/lib/python/kdoc/kdoc_parser.py-1593-\ntools/lib/python/kdoc/kdoc_parser.py:1594:        # Check if it's a SYSCALL_DEFINE0\ntools/lib/python/kdoc/kdoc_parser.py:1595:        if 'SYSCALL_DEFINE0' in proto:\ntools/lib/python/kdoc/kdoc_parser.py-1596-            is_void = True\ntools/lib/python/kdoc/kdoc_parser.py-1597-\ntools/lib/python/kdoc/kdoc_parser.py:1598:        # Replace SYSCALL_DEFINE with correct return type \u0026 function name\ntools/lib/python/kdoc/kdoc_parser.py:1599:        proto = KernRe(r'SYSCALL_DEFINE.*\\(').sub('long sys_', proto)\ntools/lib/python/kdoc/kdoc_parser.py-1600-\n--\ntools/lib/python/kdoc/kdoc_parser.py-1690-            #\ntools/lib/python/kdoc/kdoc_parser.py:1691:            if 'SYSCALL_DEFINE' in self.entry.prototype:\ntools/lib/python/kdoc/kdoc_parser.py-1692-                self.entry.prototype = self.syscall_munge(ln,\n"
}

11/2 2026/10/08 12:31 llm patch-evaluator
1m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 264003
							output: 54
							thoughts: 475
						

Thoughts:
**Decoding the System Call Naming Convention**

Alright, let's break this down. First, the regex substitution: `proto = KernRe(r'SYSCALL_DEFINE.*\(').sub('long sys_', proto)`. This confirms that a macro like `SYSCALL_DEFINE3(read, ...)` becomes `long sys_read(...)`. So, as I initially thought, the `function_name` in the kdoc for such a system call *should* be `sys_read`.

But wait a minute! Let's dive deeper. I'm looking at `include/linux/syscalls.h` now, specifically the definition of `__KAPI_SYSCALL_DEFINEx`. Aha! It's using `"sys" #name` within `kapi_get_spec`. With `SYSCALL_DEFINE3(read, ...)`, that means we're getting `kapi_get_spec("sysread")` and *not* `sys_read`. No underscore there.

Hold on, though... there's a crucial piece missing. This comes into focus with `__SYSCALL_DEFINEx` itself. We have:
```c
#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__)
```
So we have both `__se_sys##name` and `__do_sys##name` defined, and then `__KAPI_SYSCALL_DEFINEx` is used. But what exactly is `name` in relation to this? This needs to be checked when `__SYSCALL_DEFINEx` is called.

Okay, time to hunt down those calls. I'll need to search `include/linux/syscalls.h` and also `arch/x86/include/asm/syscall_wrapper.h` to see how `__SYSCALL_DEFINEx` is actually being used. This should clear up whether the naming is `_read` or `read`. That's where the answer lies.



12/2 2026/10/08 12:32 tool grepper
0m Args:
{
  "Expression": "__SYSCALL_DEFINEx",
  "PathPrefixes": [
    "include/linux/syscalls.h",
    "arch/x86/include/asm/syscall_wrapper.h"
  ]
}

Results:
{
  "Output": "arch/x86/include/asm/syscall_wrapper.h=12=extern long __ia32_sys_ni_syscall(const struct pt_regs *regs);\n--\narch/x86/include/asm/syscall_wrapper.h-14-/*\narch/x86/include/asm/syscall_wrapper.h:15: * Instead of the generic __SYSCALL_DEFINEx() definition, the x86 version takes\narch/x86/include/asm/syscall_wrapper.h-16- * struct pt_regs *regs as the only argument of the syscall stub(s) named as:\n--\narch/x86/include/asm/syscall_wrapper.h-222-\narch/x86/include/asm/syscall_wrapper.h:223:#define __SYSCALL_DEFINEx(x, name, ...)\t\t\t\t\t\\\narch/x86/include/asm/syscall_wrapper.h-224-\tstatic long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));\t\\\n--\ninclude/linux/syscalls.h=81=struct file_attr;\n--\ninclude/linux/syscalls.h-103- * It may be useful for an architecture to override the definitions of the\ninclude/linux/syscalls.h:104: * SYSCALL_DEFINE0() and __SYSCALL_DEFINEx() macros, in particular to use a\ninclude/linux/syscalls.h-105- * different calling convention for syscalls. To allow for that, the prototypes\n--\ninclude/linux/syscalls.h=215=static inline int is_syscall_trace_event(struct trace_event_call *tp_event)\n--\ninclude/linux/syscalls.h-239-\tSYSCALL_METADATA(sname, x, __VA_ARGS__)\t\t\t\\\ninclude/linux/syscalls.h:240:\t__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)\ninclude/linux/syscalls.h-241-\n--\ninclude/linux/syscalls.h-283- */\ninclude/linux/syscalls.h:284:#ifndef __SYSCALL_DEFINEx\ninclude/linux/syscalls.h:285:#define __SYSCALL_DEFINEx(x, name, ...)\t\t\t\t\t\\\ninclude/linux/syscalls.h-286-\t__diag_push();\t\t\t\t\t\t\t\\\n--\ninclude/linux/syscalls.h-307-\tstatic inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))\ninclude/linux/syscalls.h:308:#endif /* __SYSCALL_DEFINEx */\ninclude/linux/syscalls.h-309-\n"
}

13/2 2026/10/08 12:32 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 265702
							output: 36
							thoughts: 52
						

Thoughts:
Here's my analysis of what I'm thinking:

**Examining the SYSCALL_DEFINE Macro's Parameter Passing**

Okay, time to dig into the kernel's syscall implementation. I need to understand how `SYSCALL_DEFINE` handles passing the `name` argument to the underlying `__SYSCALL_DEFINEx` macro family. It's crucial for understanding how the system call interface is defined and how arguments are ultimately passed to the system call handlers. I'm going to start by consulting `include/linux/syscalls.h` specifically from lines 230 to 245. This should give me a good handle on how the core macros are constructed and what's going on under the hood. I expect to see the macro expansion unfolding there, showing how the system call name and arguments are integrated into the final function definition.



14/2 2026/10/08 12:32 tool read-file
0m Args:
{
  "File": "include/linux/syscalls.h",
  "FirstLine": 220,
  "LineCount": 35
}

Results:
{
  "Contents": " 220:\t\n 221:\t#ifndef SYSCALL_DEFINE0\n 222:\t#define SYSCALL_DEFINE0(sname)\t\t\t\t\t\\\n 223:\t\tSYSCALL_METADATA(_##sname, 0);\t\t\t\t\\\n 224:\t\tasmlinkage long sys_##sname(void);\t\t\t\\\n 225:\t\tALLOW_ERROR_INJECTION(sys_##sname, ERRNO);\t\t\\\n 226:\t\tasmlinkage long sys_##sname(void)\n 227:\t#endif /* SYSCALL_DEFINE0 */\n 228:\t\n 229:\t#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)\n 230:\t#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)\n 231:\t#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)\n 232:\t#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)\n 233:\t#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)\n 234:\t#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)\n 235:\t\n 236:\t#define SYSCALL_DEFINE_MAXARGS\t6\n 237:\t\n 238:\t#define SYSCALL_DEFINEx(x, sname, ...)\t\t\t\t\\\n 239:\t\tSYSCALL_METADATA(sname, x, __VA_ARGS__)\t\t\t\\\n 240:\t\t__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)\n 241:\t\n 242:\t#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)\n 243:\t\n 244:\t#ifdef CONFIG_KAPI_RUNTIME_CHECKS\n 245:\t/*\n 246:\t * Convert integers as (s64)(a) does, and pointers through unsigned long so\n 247:\t * they are not sign-extended on 32-bit. (unsigned long)(t)0 is an integer\n 248:\t * constant expression only if t is an integer type.\n 249:\t */\n 250:\t#define __SC_CAST_TO_S64(t, a)\t\t\t\t\t\t\\\n 251:\t\t((__force s64)__builtin_choose_expr(\t\t\t\t\\\n 252:\t\t\t__is_constexpr((__force unsigned long)(t)0),\t\t\\\n 253:\t\t\t(a), (__force unsigned long)(a)))\n 254:\t\n"
}

15/2 2026/10/08 12:32 llm patch-evaluator
1m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 266717
							output: 40
							thoughts: 995
						

Thoughts:
**Analyzing Kernel API Validation on Syscall Invocation**

Okay, so I've been tracing the kernel's syscall handling and I think I've figured out how the API validation works. Let's start with `SYSCALL_DEFINE3(read, ...)` and its path through the macros. It eventually calls `SYSCALL_DEFINEx`, which then resolves the syscall name to `_read`. This name then gets passed into `__SYSCALL_DEFINEx`, and the key piece is `kapi_get_spec("sys" #name)`. The preprocessor concatenates this into `"sys_read"`.  That string is then used to find a matching `__kapi_spec_sys_read`. This confirms that the macro naming convention works as expected for resolving the API specification.

Now, let's drill down into what `kapi_get_spec` does.  It's pretty straightforward, it takes the name (e.g., "sys_read"), locks `api_spec_mutex`, looks up the spec, unlocks the mutex, and returns the result. But I just realized something significant – this function is called inside `__do_kapi_sys_...` which, in turn, is called *on every single system call*. This means that the kernel acquires the `api_spec_mutex` for EVERY syscall invocation!  That's a potential bottleneck.

The most important part is that if a spec is found (and it will be for the commonly used syscalls like `read`, `write`, `open`, `close`, `madvise`, etc.), it calls `kapi_validate_syscall_params`. So this will be called for all the syscalls we're interested in.

Looking at `kapi_validate_syscall_params` in `kernel/api/kernel_api_spec.c`, it iterates through the parameters defined in the kernel API spec. For each parameter, it calls `kapi_validate_param_with_context`. If a validation fails and it's a syscall, it returns `-EINVAL` to userspace. I'll need to look more closely at `kapi_validate_param_with_context`, specifically around lines 1050-1200, to understand how the individual parameters are actually validated. But the overall structure is clear now: lock, lookup, validate params, call the original syscall, and validate the return value.



16/2 2026/10/08 12:33 tool read-file
0m Args:
{
  "File": "kernel/api/kernel_api_spec.c",
  "FirstLine": 1040,
  "LineCount": 80
}

Results:
{
  "Contents": "1040:\t */\n1041:\tstatic bool kapi_validate_param_checks(const struct kapi_param_spec *param_spec,\n1042:\t\t\t\t\t       s64 value, bool check_user_ptr)\n1043:\t{\n1044:\t\tint i;\n1045:\t\n1046:\t\t/* Special handling for file descriptor type */\n1047:\t\tif (param_spec-\u003etype == KAPI_TYPE_FD \u0026\u0026\n1048:\t\t    !(param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL)) {\n1049:\t\t\tif (value \u003c INT_MIN || value \u003e INT_MAX) {\n1050:\t\t\t\tpr_warn_ratelimited(\"Parameter %s: file descriptor %lld out of int range\\n\",\n1051:\t\t\t\t\tparam_spec-\u003ename, value);\n1052:\t\t\t\treturn false;\n1053:\t\t\t}\n1054:\t\t\tif (!kapi_validate_fd((int)value)) {\n1055:\t\t\t\tpr_warn_ratelimited(\"Parameter %s: invalid file descriptor %lld\\n\",\n1056:\t\t\t\t\tparam_spec-\u003ename, value);\n1057:\t\t\t\treturn false;\n1058:\t\t\t}\n1059:\t\t}\n1060:\t\n1061:\t\t/* Special handling for user pointer type */\n1062:\t\tif (check_user_ptr \u0026\u0026 param_spec-\u003etype == KAPI_TYPE_USER_PTR) {\n1063:\t\t\tconst void __user *ptr = (const void __user *)(unsigned long)value;\n1064:\t\n1065:\t\t\t/* NULL is allowed for optional parameters */\n1066:\t\t\tif (!ptr \u0026\u0026 (param_spec-\u003eflags \u0026 KAPI_PARAM_OPTIONAL))\n1067:\t\t\t\treturn true;\n1068:\t\n1069:\t\t\tif (!kapi_validate_user_ptr(ptr, param_spec-\u003esize)) {\n1070:\t\t\t\tpr_warn_ratelimited(\"Parameter %s: invalid user pointer %p (size: %zu)\\n\",\n1071:\t\t\t\t\tparam_spec-\u003ename, ptr, param_spec-\u003esize);\n1072:\t\t\t\treturn false;\n1073:\t\t\t}\n1074:\t\t}\n1075:\t\n1076:\t\t/* Special handling for path type */\n1077:\t\tif (param_spec-\u003etype == KAPI_TYPE_PATH) {\n1078:\t\t\tconst char __user *path = (const char __user *)(unsigned long)value;\n1079:\t\n1080:\t\t\tif (!kapi_validate_path(path, param_spec))\n1081:\t\t\t\treturn false;\n1082:\t\t}\n1083:\t\n1084:\t\tswitch (param_spec-\u003econstraint_type) {\n1085:\t\tcase KAPI_CONSTRAINT_NONE:\n1086:\t\tcase KAPI_CONSTRAINT_BUFFER:\n1087:\t\t\treturn true;\n1088:\t\n1089:\t\tcase KAPI_CONSTRAINT_RANGE:\n1090:\t\t\t/*\n1091:\t\t\t * If max_value is below min_value, it was likely set from an\n1092:\t\t\t * unsigned constant (e.g. SIZE_MAX) that overflowed s64.  Treat\n1093:\t\t\t * as no upper bound; only check the minimum.\n1094:\t\t\t */\n1095:\t\t\tif (param_spec-\u003emax_value \u003e= param_spec-\u003emin_value) {\n1096:\t\t\t\tif (value \u003c param_spec-\u003emin_value ||\n1097:\t\t\t\t    value \u003e param_spec-\u003emax_value) {\n1098:\t\t\t\t\tpr_warn_ratelimited(\"Parameter %s value %lld out of range [%lld, %lld]\\n\",\n1099:\t\t\t\t\t\tparam_spec-\u003ename, value,\n1100:\t\t\t\t\t\tparam_spec-\u003emin_value,\n1101:\t\t\t\t\t\tparam_spec-\u003emax_value);\n1102:\t\t\t\t\treturn false;\n1103:\t\t\t\t}\n1104:\t\t\t} else {\n1105:\t\t\t\tif (value \u003c param_spec-\u003emin_value) {\n1106:\t\t\t\t\tpr_warn_ratelimited(\"Parameter %s value %lld below minimum %lld\\n\",\n1107:\t\t\t\t\t\tparam_spec-\u003ename, value,\n1108:\t\t\t\t\t\tparam_spec-\u003emin_value);\n1109:\t\t\t\t\treturn false;\n1110:\t\t\t\t}\n1111:\t\t\t}\n1112:\t\t\treturn true;\n1113:\t\n1114:\t\tcase KAPI_CONSTRAINT_MASK:\n1115:\t\t\tif (value \u0026 ~param_spec-\u003evalid_mask) {\n1116:\t\t\t\tpr_warn_ratelimited(\"Parameter %s value 0x%llx contains invalid bits (valid mask: 0x%llx)\\n\",\n1117:\t\t\t\t\tparam_spec-\u003ename, value, param_spec-\u003evalid_mask);\n1118:\t\t\t\treturn false;\n1119:\t\t\t}\n"
}

17/2 2026/10/08 12:33 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 269548
							output: 38
							thoughts: 1904
						

Thoughts:
**Kernel API Specification Framework Analysis**

Okay, so I'm looking at this new kernel API specification framework. It's a significant addition, intercepting system calls before they execute to perform runtime validation of parameters and return values. This includes a debugfs interface for inspecting specifications (text and JSON), dynamic spec registration, and tracepoints. It touches core syscalls, like `read`, `write`, `open`, `close`, and `madvise`.

Given the fuzzing guidelines, the answer is *clearly* `WorthFuzzing=true`. This patch modifies reachable kernel code, specifically syscall handling, adds a new kernel subsystem, and introduces runtime validations.  It's definitely in a "Core Subsystem" area as defined in the fuzzing guidelines and clearly touches on "Runtime Assertions and Defensive Checks".

Now, onto the specifics: figuring out the configuration requirements and relevant symbols.

First, the configurations.  Based on the `Kconfig` files, I need to enable: `KAPI_SPEC`, `KAPI_RUNTIME_CHECKS`, and `KAPI_SPEC_DEBUGFS`. `KAPI_SPEC` is fundamental, enabling the core framework. `KAPI_RUNTIME_CHECKS` activates the runtime validation, and `KAPI_SPEC_DEBUGFS` enables the debugfs interface. The dependencies mean that `DEBUG_KERNEL` and `DEBUG_FS` should be available. While `DEBUG_FS` is part of standard debug configurations, mentioning `KAPI_SPEC`, `KAPI_RUNTIME_CHECKS`, and `KAPI_SPEC_DEBUGFS` is definitely key.

Next, I need to identify `FocusSymbols`. I need to avoid ubiquitous functions (like allocators) and focus on specific feature logic and entry points. I should focus on non-static symbols or exported functions in `kernel/api/kernel_api_spec.c` and `kernel/api/kapi_debugfs.c`. I need to extract any non-static symbols or exported functions from those two files.



18/2 2026/10/08 12:34 tool grepper
0m Args:
{
  "Expression": "^[a-zA-Z0-9_]+ .*\\(",
  "PathPrefixes": [
    "kernel/api/"
  ]
}

Results:
{
  "Output": "\nFull output is too long, showing 200 out of 361 lines.\nUse more precise expression or PathPrefixes if possible.\n\nNumber of matching lines per file (4 files in total):\nkernel/api/internal.h:3\nkernel/api/kapi_debugfs.c:12\nkernel/api/kapi_kunit.c:38\nkernel/api/kernel_api_spec.c:32\n\nkernel/api/internal.h=19=extern const struct kernel_api_spec * const __stop_kapi_specs[];\nkernel/api/internal.h-20-\nkernel/api/internal.h:21:const char *kapi_param_type_to_string(enum kapi_param_type type);\nkernel/api/internal.h:22:const char *kapi_lock_type_to_string(enum kapi_lock_type type);\nkernel/api/internal.h:23:const char *kapi_lock_scope_to_string(enum kapi_lock_scope scope);\nkernel/api/internal.h-24-\n--\nkernel/api/kapi_debugfs.c-21-/* Helper to print parameter flags */\nkernel/api/kapi_debugfs.c:22:static void print_param_flags(struct seq_file *m, u32 flags)\nkernel/api/kapi_debugfs.c-23-{\n--\nkernel/api/kapi_debugfs.c-46-/* Helper to print context flags */\nkernel/api/kapi_debugfs.c:47:static void print_context_flags(struct seq_file *m, u32 flags)\nkernel/api/kapi_debugfs.c-48-{\n--\nkernel/api/kapi_debugfs.c-72- */\nkernel/api/kapi_debugfs.c:73:static void print_block(struct seq_file *m, const char *text)\nkernel/api/kapi_debugfs.c-74-{\n--\nkernel/api/kapi_debugfs.c-87-/* Helper to print a list of signed values */\nkernel/api/kapi_debugfs.c:88:static void print_s64_list(struct seq_file *m, const char *label,\nkernel/api/kapi_debugfs.c-89-\t\t\t   const s64 *vals, u32 count)\n--\nkernel/api/kapi_debugfs.c-102-/* Helper to print which parameter carries a buffer's element count */\nkernel/api/kapi_debugfs.c:103:static void print_size_param(struct seq_file *m,\nkernel/api/kapi_debugfs.c-104-\t\t\t     const struct kernel_api_spec *spec,\n--\nkernel/api/kapi_debugfs.c-120-/* Show function for individual API spec */\nkernel/api/kapi_debugfs.c:121:static int kapi_spec_show(struct seq_file *m, void *v)\nkernel/api/kapi_debugfs.c-122-{\n--\nkernel/api/kapi_debugfs.c-435-\nkernel/api/kapi_debugfs.c:436:static int kapi_spec_open(struct inode *inode, struct file *file)\nkernel/api/kapi_debugfs.c-437-{\n--\nkernel/api/kapi_debugfs.c=441=static const struct file_operations kapi_spec_fops = {\n--\nkernel/api/kapi_debugfs.c-452- */\nkernel/api/kapi_debugfs.c:453:static int kapi_spec_json_show(struct seq_file *m, void *v)\nkernel/api/kapi_debugfs.c-454-{\n--\nkernel/api/kapi_debugfs.c-475-\nkernel/api/kapi_debugfs.c:476:static int kapi_spec_json_open(struct inode *inode, struct file *file)\nkernel/api/kapi_debugfs.c-477-{\n--\nkernel/api/kapi_debugfs.c=481=static const struct file_operations kapi_spec_json_fops = {\n--\nkernel/api/kapi_debugfs.c-494- */\nkernel/api/kapi_debugfs.c:495:static int kapi_list_show(struct seq_file *m, void *v)\nkernel/api/kapi_debugfs.c-496-{\n--\nkernel/api/kapi_debugfs.c-515-\nkernel/api/kapi_debugfs.c:516:static int kapi_list_open(struct inode *inode, struct file *file)\nkernel/api/kapi_debugfs.c-517-{\n--\nkernel/api/kapi_debugfs.c=521=static const struct file_operations kapi_list_fops = {\n--\nkernel/api/kapi_debugfs.c-527-\nkernel/api/kapi_debugfs.c:528:static int __init kapi_debugfs_init(void)\nkernel/api/kapi_debugfs.c-529-{\n--\nkernel/api/kapi_kunit.c-18-\nkernel/api/kapi_kunit.c:19:static void init_test_spec(struct kernel_api_spec *spec, const char *name)\nkernel/api/kapi_kunit.c-20-{\n--\nkernel/api/kapi_kunit.c-27-/* kapi_register_spec with valid spec returns 0 */\nkernel/api/kapi_kunit.c:28:static void test_register_valid(struct kunit *test)\nkernel/api/kapi_kunit.c-29-{\n--\nkernel/api/kapi_kunit.c-45-/* kapi_get_spec returns registered spec */\nkernel/api/kapi_kunit.c:46:static void test_lookup_registered(struct kunit *test)\nkernel/api/kapi_kunit.c-47-{\n--\nkernel/api/kapi_kunit.c-67-/* Double registration returns -EEXIST */\nkernel/api/kapi_kunit.c:68:static void test_double_register(struct kunit *test)\nkernel/api/kapi_kunit.c-69-{\n--\nkernel/api/kapi_kunit.c-88-/* Unregister makes spec unfindable */\nkernel/api/kapi_kunit.c:89:static void test_unregister(struct kunit *test)\nkernel/api/kapi_kunit.c-90-{\n--\nkernel/api/kapi_kunit.c-111-/* kapi_get_spec(NULL) returns NULL */\nkernel/api/kapi_kunit.c:112:static void test_get_spec_null(struct kunit *test)\nkernel/api/kapi_kunit.c-113-{\n--\nkernel/api/kapi_kunit.c-120-/* kapi_register_spec(NULL) returns -EINVAL */\nkernel/api/kapi_kunit.c:121:static void test_register_null(struct kunit *test)\nkernel/api/kapi_kunit.c-122-{\n--\nkernel/api/kapi_kunit.c-129-/* Spec with a NULL name is rejected */\nkernel/api/kapi_kunit.c:130:static void test_register_null_name(struct kunit *test)\nkernel/api/kapi_kunit.c-131-{\n--\nkernel/api/kapi_kunit.c-147-/* RANGE constraint - value in range is valid */\nkernel/api/kapi_kunit.c:148:static void test_constraint_range_valid(struct kunit *test)\nkernel/api/kapi_kunit.c-149-{\n--\nkernel/api/kapi_kunit.c-172-/* RANGE constraint - value out of range is invalid */\nkernel/api/kapi_kunit.c:173:static void test_constraint_range_invalid(struct kunit *test)\nkernel/api/kapi_kunit.c-174-{\n--\nkernel/api/kapi_kunit.c-192-/* MASK constraint - valid bits pass */\nkernel/api/kapi_kunit.c:193:static void test_constraint_mask_valid(struct kunit *test)\nkernel/api/kapi_kunit.c-194-{\n--\nkernel/api/kapi_kunit.c-206-/* MASK constraint - extra bits fail */\nkernel/api/kapi_kunit.c:207:static void test_constraint_mask_invalid(struct kunit *test)\nkernel/api/kapi_kunit.c-208-{\n--\nkernel/api/kapi_kunit.c-219-/* POWER_OF_TWO constraint */\nkernel/api/kapi_kunit.c:220:static void test_constraint_power_of_two(struct kunit *test)\nkernel/api/kapi_kunit.c-221-{\n--\nkernel/api/kapi_kunit.c-236-/* PAGE_ALIGNED constraint */\nkernel/api/kapi_kunit.c:237:static void test_constraint_page_aligned(struct kunit *test)\nkernel/api/kapi_kunit.c-238-{\n--\nkernel/api/kapi_kunit.c-251-/* NONZERO constraint */\nkernel/api/kapi_kunit.c:252:static void test_constraint_nonzero(struct kunit *test)\nkernel/api/kapi_kunit.c-253-{\n--\nkernel/api/kapi_kunit.c-264-/* Return value validation - success */\nkernel/api/kapi_kunit.c:265:static void test_return_validation(struct kunit *test)\nkernel/api/kapi_kunit.c-266-{\n--\nkernel/api/kapi_kunit.c-280-/* Return value validation - known error */\nkernel/api/kapi_kunit.c:281:static void test_return_known_error(struct kunit *test)\nkernel/api/kapi_kunit.c-282-{\n--\nkernel/api/kapi_kunit.c-299-/* Return value validation - unknown error */\nkernel/api/kapi_kunit.c:300:static void test_return_unknown_error(struct kunit *test)\nkernel/api/kapi_kunit.c-301-{\n--\nkernel/api/kapi_kunit.c-320-/* ALIGNMENT constraint */\nkernel/api/kapi_kunit.c:321:static void test_constraint_alignment(struct kunit *test)\nkernel/api/kapi_kunit.c-322-{\n--\nkernel/api/kapi_kunit.c-336-/* FD validation rejects values \u003e INT_MAX */\nkernel/api/kapi_kunit.c:337:static void test_fd_int_overflow(struct kunit *test)\nkernel/api/kapi_kunit.c-338-{\n--\nkernel/api/kapi_kunit.c=350=static const s64 test_enum_vals[] = { 1, 5, 10 };\nkernel/api/kapi_kunit.c-351-\nkernel/api/kapi_kunit.c:352:static void test_constraint_enum(struct kunit *test)\nkernel/api/kapi_kunit.c-353-{\n--\nkernel/api/kapi_kunit.c-369-/* BUFFER constraint always accepts (size checked at runtime) */\nkernel/api/kapi_kunit.c:370:static void test_constraint_buffer(struct kunit *test)\nkernel/api/kapi_kunit.c-371-{\n--\nkernel/api/kapi_kunit.c-382-/* RETURN_RANGE check type */\nkernel/api/kapi_kunit.c:383:static void test_return_range(struct kunit *test)\nkernel/api/kapi_kunit.c-384-{\n--\nkernel/api/kapi_kunit.c-400-\nkernel/api/kapi_kunit.c:401:static void init_dyn_buf_param(struct kapi_param_spec *param)\nkernel/api/kapi_kunit.c-402-{\n--\nkernel/api/kapi_kunit.c-408-\nkernel/api/kapi_kunit.c:409:static bool validate_dyn_buf(const struct kapi_param_spec *param,\nkernel/api/kapi_kunit.c-410-\t\t\t     const void __user *ptr, s64 count)\n--\nkernel/api/kapi_kunit.c-418-/* Dynamic buffer: any pointer is accepted when the size is 0 */\nkernel/api/kapi_kunit.c:419:static void test_dyn_buf_zero_count(struct kunit *test)\nkernel/api/kapi_kunit.c-420-{\n--\nkernel/api/kapi_kunit.c-431-/* Dynamic buffer: NULL is rejected when the size is non-zero */\nkernel/api/kapi_kunit.c:432:static void test_dyn_buf_null_nonzero_count(struct kunit *test)\nkernel/api/kapi_kunit.c-433-{\n--\nkernel/api/kapi_kunit.c-446- */\nkernel/api/kapi_kunit.c:447:static bool kapi_rejects_kernel_addr(const void __user *addr)\nkernel/api/kapi_kunit.c-448-{\n--\nkernel/api/kapi_kunit.c-451-\nkernel/api/kapi_kunit.c:452:static bool kapi_bounds_user_range(const void __user *base)\nkernel/api/kapi_kunit.c-453-{\n--\nkernel/api/kapi_kunit.c-457-/* Dynamic buffer: addresses outside user space are rejected when the size is non-zero */\nkernel/api/kapi_kunit.c:458:static void test_dyn_buf_kernel_addr(struct kunit *test)\nkernel/api/kapi_kunit.c-459-{\n--\nkernel/api/kapi_kunit.c-473-/* Dynamic buffer: an unset size_multiplier means byte units */\nkernel/api/kapi_kunit.c:474:static void test_dyn_buf_default_multiplier(struct kunit *test)\nkernel/api/kapi_kunit.c-475-{\n--\nkernel/api/kapi_kunit.c-491-/* Dynamic buffer: explicit multiplier scales the size, negative and overflowing counts fail */\nkernel/api/kapi_kunit.c:492:static void test_dyn_buf_multiplier_and_bad_count(struct kunit *test)\nkernel/api/kapi_kunit.c-493-{\n--\nkernel/api/kapi_kunit.c=513=static const struct kernel_api_spec kapi_test_macro_spec = {\n--\nkernel/api/kapi_kunit.c-525-/* Signal mask and capability alternative macros fill in their counts */\nkernel/api/kapi_kunit.c:526:static void test_signal_mask_and_cap_alternative(struct kunit *test)\nkernel/api/kapi_kunit.c-527-{\n--\nkernel/api/kapi_kunit.c-544-/* Unregister non-existent spec is a no-op */\nkernel/api/kapi_kunit.c:545:static void test_unregister_nonexistent(struct kunit *test)\nkernel/api/kapi_kunit.c-546-{\n--\n\n"
}

19/2 2026/10/08 12:34 llm patch-evaluator
1m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 275953
							output: 45
							thoughts: 21
						

Thoughts:
Here's a summary of my thought process, as requested:

**Diving into the Kernel API Spec**

Okay, time to get my hands dirty. I need to understand how this kernel API is structured and what specific functions are available. My first step is to locate and examine the `kernel/api/kernel_api_spec.c` file. That's usually the place where the core function definitions and their specifications will be located. I'll need to meticulously go through this code. I'm looking for the structure, the prototypes, the comments - anything that will illuminate the intended use and behaviour of each function. Knowing the exact function definitions is fundamental. From that, I can form a solid understanding of how I can interface with the kernel.



20/2 2026/10/08 12:35 tool grepper
0m Args:
{
  "Expression": "^[a-zA-Z0-9_].*\\(",
  "PathPrefixes": [
    "kernel/api/kernel_api_spec.c"
  ]
}

Results:
{
  "Output": "kernel/api/kernel_api_spec.c-32-/* Dynamic API registration */\nkernel/api/kernel_api_spec.c:33:static LIST_HEAD(dynamic_api_specs);\nkernel/api/kernel_api_spec.c:34:static DEFINE_MUTEX(api_spec_mutex);\nkernel/api/kernel_api_spec.c-35-\nkernel/api/kernel_api_spec.c=36=struct dynamic_api_spec {\n--\nkernel/api/kernel_api_spec.c-43- */\nkernel/api/kernel_api_spec.c:44:static const struct kernel_api_spec *__kapi_find_spec_locked(const char *name)\nkernel/api/kernel_api_spec.c-45-{\n--\nkernel/api/kernel_api_spec.c-76- */\nkernel/api/kernel_api_spec.c:77:const struct kernel_api_spec *kapi_get_spec(const char *name)\nkernel/api/kernel_api_spec.c-78-{\n--\nkernel/api/kernel_api_spec.c-89-}\nkernel/api/kernel_api_spec.c:90:EXPORT_SYMBOL_GPL(kapi_get_spec);\nkernel/api/kernel_api_spec.c-91-\n--\nkernel/api/kernel_api_spec.c-97- */\nkernel/api/kernel_api_spec.c:98:int kapi_register_spec(const struct kernel_api_spec *spec)\nkernel/api/kernel_api_spec.c-99-{\n--\nkernel/api/kernel_api_spec.c-125-}\nkernel/api/kernel_api_spec.c:126:EXPORT_SYMBOL_GPL(kapi_register_spec);\nkernel/api/kernel_api_spec.c-127-\n--\nkernel/api/kernel_api_spec.c-131- */\nkernel/api/kernel_api_spec.c:132:void kapi_unregister_spec(const char *name)\nkernel/api/kernel_api_spec.c-133-{\n--\nkernel/api/kernel_api_spec.c-149-}\nkernel/api/kernel_api_spec.c:150:EXPORT_SYMBOL_GPL(kapi_unregister_spec);\nkernel/api/kernel_api_spec.c-151-\n--\nkernel/api/kernel_api_spec.c-157- */\nkernel/api/kernel_api_spec.c:158:const char *kapi_param_type_to_string(enum kapi_param_type type)\nkernel/api/kernel_api_spec.c-159-{\n--\nkernel/api/kernel_api_spec.c-187- */\nkernel/api/kernel_api_spec.c:188:const char *kapi_lock_type_to_string(enum kapi_lock_type type)\nkernel/api/kernel_api_spec.c-189-{\n--\nkernel/api/kernel_api_spec.c-212- */\nkernel/api/kernel_api_spec.c:213:const char *kapi_lock_scope_to_string(enum kapi_lock_scope scope)\nkernel/api/kernel_api_spec.c-214-{\n--\nkernel/api/kernel_api_spec.c-233- */\nkernel/api/kernel_api_spec.c:234:static const char *return_check_type_to_string(enum kapi_return_check_type type)\nkernel/api/kernel_api_spec.c-235-{\n--\nkernel/api/kernel_api_spec.c-256- */\nkernel/api/kernel_api_spec.c:257:static const char *capability_action_to_string(enum kapi_capability_action action)\nkernel/api/kernel_api_spec.c-258-{\n--\nkernel/api/kernel_api_spec.c-280- */\nkernel/api/kernel_api_spec.c:281:static const char *constraint_type_to_string(enum kapi_constraint_type type)\nkernel/api/kernel_api_spec.c-282-{\n--\nkernel/api/kernel_api_spec.c-313- */\nkernel/api/kernel_api_spec.c:314:static int kapi_json_escape(char *buf, size_t size, const char *str)\nkernel/api/kernel_api_spec.c-315-{\n--\nkernel/api/kernel_api_spec.c-356-\nkernel/api/kernel_api_spec.c:357:static int kapi_json_str(char *buf, size_t size, const char *str)\nkernel/api/kernel_api_spec.c-358-{\n--\nkernel/api/kernel_api_spec.c-366-\nkernel/api/kernel_api_spec.c:367:static int kapi_json_s64_list(char *buf, size_t size, const s64 *vals, u32 count)\nkernel/api/kernel_api_spec.c-368-{\n--\nkernel/api/kernel_api_spec.c-379-\nkernel/api/kernel_api_spec.c:380:static int kapi_json_struct_spec(char *buf, size_t size,\nkernel/api/kernel_api_spec.c-381-\t\t\t\t const struct kapi_struct_spec *st)\n--\nkernel/api/kernel_api_spec.c-438- */\nkernel/api/kernel_api_spec.c:439:int kapi_export_json(const struct kernel_api_spec *spec, char *buf, size_t size)\nkernel/api/kernel_api_spec.c-440-{\n--\nkernel/api/kernel_api_spec.c-749-}\nkernel/api/kernel_api_spec.c:750:EXPORT_SYMBOL_GPL(kapi_export_json);\nkernel/api/kernel_api_spec.c-751-\n--\nkernel/api/kernel_api_spec.c-761- */\nkernel/api/kernel_api_spec.c:762:static void kapi_trace_format_params(const struct kernel_api_spec *spec,\nkernel/api/kernel_api_spec.c-763-\t\t\t\t     const s64 *args, int nargs,\n--\nkernel/api/kernel_api_spec.c-803- */\nkernel/api/kernel_api_spec.c:804:static bool kapi_validate_fd(int fd)\nkernel/api/kernel_api_spec.c-805-{\n--\nkernel/api/kernel_api_spec.c-815- */\nkernel/api/kernel_api_spec.c:816:static bool kapi_validate_user_ptr(const void __user *ptr, size_t size)\nkernel/api/kernel_api_spec.c-817-{\n--\nkernel/api/kernel_api_spec.c-833- */\nkernel/api/kernel_api_spec.c:834:static bool kapi_validate_user_ptr_with_params(const struct kapi_param_spec *param_spec,\nkernel/api/kernel_api_spec.c-835-\t\t\t\t\t\tconst void __user *ptr,\n--\nkernel/api/kernel_api_spec.c-884- */\nkernel/api/kernel_api_spec.c:885:static bool kapi_validate_path(const char __user *path,\nkernel/api/kernel_api_spec.c-886-\t\t\t\tconst struct kapi_param_spec *param_spec)\n--\nkernel/api/kernel_api_spec.c-938- */\nkernel/api/kernel_api_spec.c:939:static bool kapi_validate_user_string(const char __user *str,\nkernel/api/kernel_api_spec.c-940-\t\t\t\t       const struct kapi_param_spec *param_spec)\n--\nkernel/api/kernel_api_spec.c-1009- */\nkernel/api/kernel_api_spec.c:1010:static bool kapi_validate_user_ptr_constraint(const void __user *ptr,\nkernel/api/kernel_api_spec.c-1011-\t\t\t\t\t       const struct kapi_param_spec *param_spec)\n--\nkernel/api/kernel_api_spec.c-1040- */\nkernel/api/kernel_api_spec.c:1041:static bool kapi_validate_param_checks(const struct kapi_param_spec *param_spec,\nkernel/api/kernel_api_spec.c-1042-\t\t\t\t       s64 value, bool check_user_ptr)\n--\nkernel/api/kernel_api_spec.c-1202- */\nkernel/api/kernel_api_spec.c:1203:bool kapi_validate_param(const struct kapi_param_spec *param_spec, s64 value)\nkernel/api/kernel_api_spec.c-1204-{\n--\nkernel/api/kernel_api_spec.c-1206-}\nkernel/api/kernel_api_spec.c:1207:EXPORT_SYMBOL_GPL(kapi_validate_param);\nkernel/api/kernel_api_spec.c-1208-\n--\nkernel/api/kernel_api_spec.c-1217- */\nkernel/api/kernel_api_spec.c:1218:bool kapi_validate_param_with_context(const struct kapi_param_spec *param_spec,\nkernel/api/kernel_api_spec.c-1219-\t\t\t\t       s64 value, const s64 *all_params, int param_count)\n--\nkernel/api/kernel_api_spec.c-1239-}\nkernel/api/kernel_api_spec.c:1240:EXPORT_SYMBOL_GPL(kapi_validate_param_with_context);\nkernel/api/kernel_api_spec.c-1241-\n--\nkernel/api/kernel_api_spec.c-1249- */\nkernel/api/kernel_api_spec.c:1250:int kapi_validate_syscall_params(const struct kernel_api_spec *spec,\nkernel/api/kernel_api_spec.c-1251-\t\t\t\t const s64 *params, int param_count)\n--\nkernel/api/kernel_api_spec.c-1296-}\nkernel/api/kernel_api_spec.c:1297:EXPORT_SYMBOL_GPL(kapi_validate_syscall_params);\nkernel/api/kernel_api_spec.c-1298-\n--\nkernel/api/kernel_api_spec.c-1305- */\nkernel/api/kernel_api_spec.c:1306:bool kapi_check_return_success(const struct kapi_return_spec *return_spec, s64 retval)\nkernel/api/kernel_api_spec.c-1307-{\n--\nkernel/api/kernel_api_spec.c-1343-}\nkernel/api/kernel_api_spec.c:1344:EXPORT_SYMBOL_GPL(kapi_check_return_success);\nkernel/api/kernel_api_spec.c-1345-\n--\nkernel/api/kernel_api_spec.c-1357- */\nkernel/api/kernel_api_spec.c:1358:bool kapi_validate_return_value(const struct kernel_api_spec *spec, s64 retval)\nkernel/api/kernel_api_spec.c-1359-{\n--\nkernel/api/kernel_api_spec.c-1400-}\nkernel/api/kernel_api_spec.c:1401:EXPORT_SYMBOL_GPL(kapi_validate_return_value);\nkernel/api/kernel_api_spec.c-1402-\n--\nkernel/api/kernel_api_spec.c-1410- */\nkernel/api/kernel_api_spec.c:1411:int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 retval)\nkernel/api/kernel_api_spec.c-1412-{\n--\nkernel/api/kernel_api_spec.c-1425-}\nkernel/api/kernel_api_spec.c:1426:EXPORT_SYMBOL_GPL(kapi_validate_syscall_return);\nkernel/api/kernel_api_spec.c-1427-\n--\nkernel/api/kernel_api_spec.c-1431- */\nkernel/api/kernel_api_spec.c:1432:void kapi_check_context(const struct kernel_api_spec *spec)\nkernel/api/kernel_api_spec.c-1433-{\n--\nkernel/api/kernel_api_spec.c-1467-}\nkernel/api/kernel_api_spec.c:1468:EXPORT_SYMBOL_GPL(kapi_check_context);\nkernel/api/kernel_api_spec.c-1469-\n"
}

21/2 2026/10/08 12:35 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 279497
							output: 311
							thoughts: 7677
						

Thoughts:
**Kernel API Specification Framework Fuzzing Configuration**

Okay, let's break this down for the expert. This patch series introduces the Kernel API Specification Framework (KAPI) to validate syscall arguments and return values at runtime, and exposes this information via debugfs. The core of this is the addition of runtime checks that are enabled when `CONFIG_KAPI_RUNTIME_CHECKS` is on. This means that for those syscalls that *have* a specification (currently `sys_open`, `sys_close`, `sys_read`, `sys_write`, `sys_madvise`, which are the only ones with added kernel-doc and generated `.apispec.h` files), every time they are called, the code flow will now execute `kapi_validate_syscall_params` to validate parameters, and `kapi_validate_syscall_return` to validate return values. The patch does this through the introduction of `__do_kapi_sys##name` wrappers. If the parameter validation fails, the syscall is skipped and an `-EINVAL` is immediately returned. The static check is that the parameters passed through `strnlen_user` or the size of a user pointer is valid when dereferenced.

We need to make sure we're focused on the *new* logic introduced, not the ubiquitous pieces.  Specifically, we *must not* include `kapi_get_spec` in our `FocusSymbols` list, even though it's called by every syscall, as this would dilute our fuzzing efforts (as per the fuzzing guidelines to avoid focusing on generic paths).  Instead, we must focus on the functions that implement the *new* behavior.

With that in mind, the key functions to include in `FocusSymbols` are:

*   `kapi_validate_syscall_params`: The entry point for syscall parameter validation.
*   `kapi_validate_syscall_return`: Entry point for syscall return value validation.
*   `kapi_export_json`: Exports specs to JSON (used by debugfs and kunit).
*   `kapi_register_spec`: API for dynamic spec registration.
*   `kapi_unregister_spec`: API for dynamic spec unregistration.
*   `kapi_spec_show`: Debugfs handler for plain text output.
*   `kapi_spec_json_show`: Debugfs handler for JSON output.
*   `kapi_list_show`: Debugfs handler for listing the specs.

These functions implement the core KAPI logic and provide the debug interface to access the information. Since `kapi_validate_syscall_params` is called only for syscalls that have API specs and is the central function implemented in the patch to validate parameters against constraints, it is feature-specific and added by this patch.

Furthermore, we need to enable the following configurations, as determined by the `Kconfig` files:

*   `KAPI_SPEC`
*   `KAPI_RUNTIME_CHECKS`
*   `KAPI_SPEC_DEBUGFS`
*   `DEBUG_FS`

We also note the existence of assertion handling through `WARN_ONCE`.

Finally, because this patch series introduces changes to core syscall paths (with `__do_kapi_sys`) and debugfs, the code is reachable, and we mark it for fuzzing (`WorthFuzzing = true`).



22/2 2026/10/08 12:36 tool set-results
0m Args:
{
  "EnableConfigs": [
    "KAPI_SPEC",
    "KAPI_RUNTIME_CHECKS",
    "KAPI_SPEC_DEBUGFS",
    "DEBUG_FS"
  ],
  "FocusSymbols": [
    "kapi_validate_syscall_params",
    "kapi_validate_syscall_return",
    "kapi_validate_param_with_context",
    "kapi_validate_return_value",
    "kapi_export_json",
    "kapi_register_spec",
    "kapi_unregister_spec",
    "kapi_spec_show",
    "kapi_spec_json_show",
    "kapi_list_show"
  ],
  "Reasoning": "The patch series introduces the Kernel API Specification Framework (KAPI). It modifies x86 and generic syscall wrappers (__SYSCALL_DEFINEx) to intercept syscall execution, validating syscall parameters against annotations before running the handler and validating return values afterward (initially annotating sys_open, sys_close, sys_read, sys_write, and sys_madvise). It also adds a debugfs interface under /sys/kernel/debug/kapi/ to expose human-readable and JSON-serialized specifications. This modifies reachable core syscall dispatch and VFS/mm execution paths, making it worth fuzzing. kapi_get_spec is intentionally omitted from FocusSymbols to avoid diluting coverage filters across all syscalls.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "KAPI_SPEC",
    "KAPI_RUNTIME_CHECKS",
    "KAPI_SPEC_DEBUGFS",
    "DEBUG_FS"
  ],
  "FocusSymbols": [
    "kapi_validate_syscall_params",
    "kapi_validate_syscall_return",
    "kapi_validate_param_with_context",
    "kapi_validate_return_value",
    "kapi_export_json",
    "kapi_register_spec",
    "kapi_unregister_spec",
    "kapi_spec_show",
    "kapi_spec_json_show",
    "kapi_list_show"
  ],
  "Reasoning": "The patch series introduces the Kernel API Specification Framework (KAPI). It modifies x86 and generic syscall wrappers (__SYSCALL_DEFINEx) to intercept syscall execution, validating syscall parameters against annotations before running the handler and validating return values afterward (initially annotating sys_open, sys_close, sys_read, sys_write, and sys_madvise). It also adds a debugfs interface under /sys/kernel/debug/kapi/ to expose human-readable and JSON-serialized specifications. This modifies reachable core syscall dispatch and VFS/mm execution paths, making it worth fuzzing. kapi_get_spec is intentionally omitted from FocusSymbols to avoid diluting coverage filters across all syscalls.",
  "WorthFuzzing": true
}

23/1 2026/10/08 12:36 action If
2m Args:
{
  "WorthFuzzing": true
}

Results:
null

24/2 2026/10/08 12:36 agent kmsan-evaluator
2m Model:
core

Args:
null

Results:
{
  "KMSANReasoning": "The patch series introduces the Kernel API Specification (KAPI) framework, which provides machine-readable API specifications and optional runtime validation of syscall parameters (via SYSCALL_DEFINEx wrappers), debugfs introspection, tracepoints, and userspace tooling.\n\nEvaluating KMSAN vs. KASAN applicability:\n- KMSAN detects reads of uninitialized memory and info-leaks to userspace. This patch does not introduce any uninitialized memory risks:\n  1. All API specifications compiled into the kernel are static const objects placed in .rodata, where uninitialized fields are guaranteed to be zero-initialized by the C compiler.\n  2. Dynamic registrations use kzalloc_obj, and KUnit test specs use kunit_kzalloc / memset(..., 0, ...).\n  3. No kernel structures or uninitialized paddings are copied to userspace via copy_to_user, put_user, netlink, ioctl, or socket options. The debugfs interface outputs text and JSON formatted via seq_file (scnprintf), and the tracepoint helper explicitly memsets __entry-\u003eargs.\n  4. The runtime validation hooks only perform sanity checks on syscall arguments (checking ranges, masks, and user pointer accessibility via access_ok/strnlen_user) and early-return -EINVAL on violation, without altering buffer lengths or leaving kernel data uninitialized.\n- Potential risks in this patch (e.g. out-of-bounds reads during .kapi_specs section traversal, format string truncation/buffer bounds, or deadlocks on api_spec_mutex) are already detected by standard tools like KASAN and LOCKDEP.\n\nTherefore, a dedicated KMSAN fuzzing session is not justified.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit 866c60e83179de8f813261f2eb9fcbe27010e491
Author: syz-cluster <triage@syzkaller.com>
Date:   Thu Oct 8 12:29:08 2026 +0000

    syz-cluster: applied patch under review

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/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..818c9b966a0c0
--- /dev/null
+++ b/Documentation/dev-tools/kernel-api-spec.rst
@@ -0,0 +1,648 @@
+.. 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. See
+   `The kapi Tool`_.
+
+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:``).
+* ``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``,
+  ``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
+3. Optionally enable ``CONFIG_KAPI_SPEC_DEBUGFS`` for debugfs interface
+
+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()``.
+
+.. 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
+-----------------
+
+``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,
+
+Tracepoints
+-----------
+
+When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, the syscall validation path emits
+two ftrace tracepoints (in the ``kapi`` trace system) for every syscall that has a
+specification:
+
+- ``kapi_syscall_enter`` -- fired before parameter validation, recording the spec
+  name, the raw syscall argument values, and -- when the spec provides parameter
+  metadata -- a rendered ``name=value`` list: signed integer and file descriptor
+  values are shown in decimal, all other values in hex, and an unnamed parameter
+  as ``arg``.
+- ``kapi_syscall_exit`` -- fired after the handler returns, or in place of the
+  handler when parameter validation rejects the call (the handler is skipped and
+  ``-EINVAL`` is returned). Records the spec name, the return value, and
+  ``spec_match``: 0 when the parameters were rejected or a successful ``fd`` return
+  was not a valid file descriptor, and 1 otherwise. Error codes missing from the
+  spec do not clear it.
+
+Unlike the ``pr_warn_ratelimited`` violation reports, the tracepoints capture every
+spec'd call rather than only violations, and can be filtered with the usual ftrace
+facilities. They require ``CONFIG_TRACEPOINTS`` and stay dormant
+until enabled::
+
+    # echo 1 > /sys/kernel/tracing/events/kapi/enable
+    # cat /sys/kernel/tracing/trace
+     ...  kapi_syscall_enter: sys_read(fd=3, buf=0x7ffd46780b58, count=0x340)
+     ...  kapi_syscall_exit: sys_read = 832 spec_match=1
+     ...  kapi_syscall_enter: sys_open(filename=0x480300, flags=268435456, mode=0x0)
+     ...  kapi_syscall_exit: sys_open = -22 spec_match=0
+
+DebugFS Interface
+=================
+
+The debugfs interface provides runtime access to API specifications:
+
+Directory Structure
+-------------------
+
+::
+
+    /sys/kernel/debug/kapi/
+    ├── list                     # Overview of all registered API specs
+    ├── specs/                   # Per-API specification files
+    │   ├── sys_open             # Human-readable spec for sys_open
+    │   ├── sys_close            # Human-readable spec for sys_close
+    │   ├── sys_read             # Human-readable spec for sys_read
+    │   ├── sys_write            # Human-readable spec for sys_write
+    │   └── sys_madvise          # Human-readable spec for sys_madvise
+    └── specs-json/              # Machine-readable counterpart of specs/
+        ├── sys_open             # JSON spec for sys_open
+        └── ...
+
+Usage Examples
+--------------
+
+List all available API specifications::
+
+    $ cat /sys/kernel/debug/kapi/list
+    Available Kernel API Specifications
+    ===================================
+
+    sys_open - Open or create a file
+    sys_close - Close a file descriptor
+    sys_read - Read data from a file descriptor
+    sys_write - Write data to a file descriptor
+    sys_madvise - Give advice about use of memory
+
+    Total: 5 specifications
+
+Query specific API::
+
+    $ cat /sys/kernel/debug/kapi/specs/sys_open
+    Kernel API Specification
+    ========================
+
+    Name: sys_open
+    Version: 1
+    Description: Open or create a file
+    ...
+
+The ``specs-json/`` files carry the complete specification: for each
+parameter the type class, flags and the constraint (type, range, valid
+mask, enumerated values, alignment, size and the index of the parameter
+holding a buffer's size), the return check, errors, locks, signals,
+signal masks, side effects, state transitions, capabilities, additional
+constraints and structure specifications. Masks and flag words are
+hex strings, enumerations are lower-case tokens such as
+``"constraint_type": "buffer"``, and ``size_param_idx`` is a 0-based
+index into ``parameters`` (``null`` when unused). The ``kapi`` tool
+reads these files with ``--debugfs`` and reports the same data as
+``--vmlinux``.
+
+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)
+
+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. Verify debugfs output is correct
+5. Run scripts/checkpatch.pl on your changes
diff --git a/MAINTAINERS b/MAINTAINERS
index 65e8a4b5c90b1..20020705921a6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14118,6 +14118,19 @@ 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:	include/trace/events/kapi.h
+F:	kernel/api/
+F:	tools/kapi/
+F:	tools/lib/python/kdoc/kdoc_apispec.py
+F:	tools/testing/selftests/kapi/
+
 KERNEL AUTOMOUNTER
 M:	Ian Kent <raven@themaw.net>
 L:	autofs@vger.kernel.org
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/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/fs/open.c b/fs/open.c
index 6b1c14e684a93..9f08c438f162a 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -1424,6 +1424,330 @@ int do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
 }
 
 
+/**
+ * sys_open - Open or create a file
+ * @filename: Pathname of the file to open or create
+ * @flags: File access mode and behavior flags (O_RDONLY, O_WRONLY, O_RDWR, etc.)
+ * @mode: File permission bits for newly created files (only with O_CREAT/O_TMPFILE)
+ *
+ * long-desc: Opens the file named by filename, relative to the current
+ *   working directory if the path is relative. With O_CREAT, the file is
+ *   created if it does not exist. With O_TMPFILE, filename must name an
+ *   existing directory, in which an unnamed file is created. A new file gets
+ *   mode & ~umask as its permission bits.
+ *
+ *   The low two bits of flags (O_ACCMODE) select the access mode: O_RDONLY,
+ *   O_WRONLY or O_RDWR. File creation and file status flags are ORed in.
+ *
+ *   File creation flags: O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC, O_DIRECTORY,
+ *   O_NOFOLLOW, O_CLOEXEC, O_TMPFILE, O_EMPTYPATH. O_EMPTYPATH permits an
+ *   empty filename, which for open() refers to the current working directory.
+ *
+ *   File status flags: O_APPEND, FASYNC, O_DIRECT, O_DSYNC, O_LARGEFILE,
+ *   O_NOATIME, O_NONBLOCK (O_NDELAY), O_PATH, O_SYNC. These become part of the
+ *   file's open file description and can be retrieved with fcntl(F_GETFL). Only
+ *   O_APPEND, O_NONBLOCK, O_DIRECT, O_NOATIME and FASYNC can be changed later
+ *   with fcntl(F_SETFL).
+ *
+ *   On success the lowest-numbered file descriptor not currently open in the
+ *   process is returned.
+ *
+ *   On 64-bit systems, O_LARGEFILE is automatically added to the flags. On 32-bit
+ *   systems, files larger than 2GB require O_LARGEFILE to be explicitly set.
+ *
+ *   open() is equivalent to openat(AT_FDCWD, filename, flags, mode).
+ *
+ * contexts: process, sleepable
+ *
+ * param: filename
+ *   type: path, input
+ *   constraint-type: user_path
+ *   cdesc: Must be a valid null-terminated path string in user memory.
+ *     Maximum path length is PATH_MAX (4096 bytes) including null terminator.
+ *     For relative paths, resolution starts from current working directory.
+ *     The path is followed (symlinks resolved) unless O_NOFOLLOW is specified.
+ *
+ * param: flags
+ *   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_NDELAY | O_DSYNC | O_SYNC |
+ *                         FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW |
+ *                         O_NOATIME | O_CLOEXEC | O_PATH | O_TMPFILE | O_EMPTYPATH)
+ *   cdesc: Should be one of O_RDONLY (0), O_WRONLY (1), or O_RDWR (2) as the
+ *     access mode. Additional flags may be ORed. O_CREAT combined with
+ *     O_DIRECTORY or O_TMPFILE, O_TMPFILE without O_DIRECTORY, and O_TMPFILE
+ *     with read-only mode return EINVAL. With O_PATH, open() silently drops
+ *     every other flag except O_DIRECTORY, O_NOFOLLOW, O_CLOEXEC and
+ *     O_EMPTYPATH (only openat2() rejects them with EINVAL). Unknown flags are
+ *     silently ignored for backward compatibility (unlike openat2 which
+ *     rejects them).
+ *
+ * param: mode
+ *   type: uint, input
+ *   cdesc: Only meaningful when O_CREAT or O_TMPFILE is specified in
+ *     flags. Specifies the file mode bits (permissions and setuid/setgid/sticky
+ *     bits) for a newly created file. The effective mode is (mode & ~umask).
+ *     When O_CREAT/O_TMPFILE is not set, mode is ignored. Mode values exceeding
+ *     S_IALLUGO (07777) are masked off.
+ *
+ * return:
+ *   type: int
+ *   check-type: fd
+ *   success: >= 0
+ *   desc: On success, returns a new file descriptor (non-negative integer).
+ *     The returned file descriptor is the lowest-numbered descriptor not
+ *     currently open for the process. On error, returns a negative error code.
+ *
+ * error: EACCES, Permission denied
+ *   desc: The requested access to the file is not allowed, or search permission
+ *     is denied for one of the directories in the path prefix of pathname, or
+ *     the file did not exist yet and write access to the parent directory is
+ *     not allowed, or O_TRUNC is specified but write permission is denied, or
+ *     pathname is a device special file on a nodev mount, or O_CREAT on an
+ *     existing FIFO or regular file in a sticky directory is refused by
+ *     protected_fifos or protected_regular, or a security module denies the
+ *     open.
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ *   desc: O_NONBLOCK was specified and a conflicting lease is held on the file,
+ *     so the open would have to wait for the lease to break. break_lease()
+ *     returns -EWOULDBLOCK, which has the same value as EAGAIN.
+ *
+ * error: EBUSY, Device or resource busy
+ *   desc: O_EXCL was specified in flags and pathname refers to a block device
+ *     that is in use by the system (e.g., it is mounted).
+ *
+ * error: EDQUOT, Disk quota exceeded
+ *   desc: O_CREAT is specified and the file does not exist, and the user's quota
+ *     of disk blocks or inodes on the filesystem has been exhausted.
+ *
+ * error: EEXIST, File exists
+ *   desc: O_CREAT and O_EXCL were specified in flags, but pathname already exists.
+ *     This error is atomic with respect to file creation - it prevents race
+ *     conditions (TOCTOU) when creating files.
+ *
+ * error: EFAULT, Bad address
+ *   desc: pathname points outside the process's accessible address space.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: A signal arrived while the open was blocked waiting for the partner
+ *     of a FIFO open (fifo_open), waiting for a conflicting lease to break
+ *     (__break_lease), or inside a driver's open method. The kernel-internal
+ *     -ERESTARTSYS is reported as EINTR unless the handler uses SA_RESTART.
+ *
+ * error: EINVAL, Invalid argument
+ *   desc: Returned for several conditions: (1) Invalid O_* flag combinations
+ *     (O_CREAT with O_DIRECTORY, O_CREAT with O_TMPFILE, O_TMPFILE without
+ *     O_DIRECTORY, O_TMPFILE with read-only access). (2) O_DIRECT requested
+ *     but the filesystem does not support it.
+ *
+ * error: EISDIR, Is a directory
+ *   desc: pathname refers to a directory and the access requested involved
+ *     writing (O_WRONLY, O_RDWR, or O_TRUNC). Also returned when O_CREAT is
+ *     specified and pathname names an existing directory or ends in a slash.
+ *
+ * error: ELOOP, Too many symbolic links
+ *   desc: Too many symbolic links were encountered in resolving pathname, or
+ *     O_NOFOLLOW was specified but pathname refers to a symbolic link. With
+ *     O_PATH and O_NOFOLLOW the symbolic link itself is opened instead.
+ *
+ * error: EMFILE, Too many open files
+ *   desc: The per-process limit on the number of open file descriptors has been
+ *     reached. This limit is RLIMIT_NOFILE (default typically 1024, max set by
+ *     /proc/sys/fs/nr_open).
+ *
+ * error: ENAMETOOLONG, File name too long
+ *   desc: pathname was too long, exceeding PATH_MAX (4096) bytes, or a single
+ *     path component exceeded NAME_MAX (usually 255) bytes.
+ *
+ * error: ENFILE, Too many open files in system
+ *   desc: The system-wide limit on the total number of open files has been
+ *     reached (/proc/sys/fs/file-max). Processes with CAP_SYS_ADMIN can exceed
+ *     this limit.
+ *
+ * error: ENODEV, No such device
+ *   desc: The filesystem or driver open method failed with ENODEV, or the
+ *     file's inode has no file operations assigned. A device special file with
+ *     no registered device fails with ENXIO instead.
+ *
+ * error: ENOENT, No such file or directory
+ *   desc: A directory component in pathname does not exist or is a dangling
+ *     symbolic link, or O_CREAT is not set and the named file does not exist,
+ *     or pathname is an empty string and O_EMPTYPATH is not specified.
+ *
+ * error: ENOMEM, Out of memory
+ *   desc: The kernel could not allocate sufficient memory for the file structure,
+ *     path lookup structures, or the filename buffer.
+ *
+ * error: ENOSPC, No space left on device
+ *   desc: O_CREAT was specified and the file does not exist, and the directory
+ *     or filesystem containing the file has no room for a new file entry.
+ *
+ * error: ENOTDIR, Not a directory
+ *   desc: A component used as a directory in pathname is not actually a directory,
+ *     or O_DIRECTORY was specified and pathname was not a directory.
+ *
+ * error: ENXIO, No such device or address
+ *   desc: O_NONBLOCK | O_WRONLY is set and the named file is a FIFO and no
+ *     process has the FIFO open for reading. Also returned when opening a device
+ *     special file whose device does not exist (chrdev_open, blkdev_open), or
+ *     when opening a socket inode.
+ *
+ * error: EOPNOTSUPP, Operation not supported
+ *   desc: The filesystem containing pathname does not support O_TMPFILE.
+ *
+ * error: EOVERFLOW, Value too large for defined data type
+ *   desc: pathname refers to a regular file that is too large to be opened.
+ *     This occurs on 32-bit systems without O_LARGEFILE when the file size
+ *     exceeds 2GB (2^31 - 1 bytes).
+ *
+ * error: EPERM, Operation not permitted
+ *   desc: O_NOATIME flag was specified but the effective UID of the caller did
+ *     not match the owner of the file and the caller is not privileged, or the
+ *     file is append-only and O_TRUNC was specified or write mode without
+ *     O_APPEND, or the file is immutable, or a seal prevents the operation.
+ *
+ * error: EROFS, Read-only file system
+ *   desc: pathname refers to a file on a read-only filesystem and write access
+ *     was requested.
+ *
+ * error: ETXTBSY, Text file busy
+ *   desc: Write access or O_TRUNC was requested for an executable image that
+ *     is currently being executed, or O_TRUNC was requested on an active swap
+ *     file. A swap file can otherwise be opened for writing.
+ *
+ * lock: files->file_lock
+ *   type: spinlock
+ *   acquired: true
+ *   released: true
+ *   desc: Acquired when allocating a file descriptor slot. Held briefly during
+ *     fd allocation via alloc_fd() and released before the syscall returns.
+ *
+ * lock: inode->i_rwsem (parent directory)
+ *   type: semaphore
+ *   acquired: true
+ *   released: true
+ *   desc: Conditional, taken only when the final component is not resolved by
+ *     the lockless dcache lookup. lookup_open() takes it exclusively with
+ *     inode_lock() when O_CREAT is set and shared with inode_lock_shared()
+ *     otherwise. Slow-path lookup of path components takes it shared. Released
+ *     when the lookup returns. The open path has no killable variant.
+ *
+ * lock: RCU read-side
+ *   type: rcu
+ *   acquired: true
+ *   released: true
+ *   desc: Path lookup uses RCU mode initially for performance. If RCU lookup
+ *     fails (returns -ECHILD), falls back to reference-based lookup.
+ *
+ * signal: Any signal
+ *   direction: receive
+ *   action: return
+ *   condition: When blocked in an interruptible wait
+ *   desc: The syscall may be interrupted while waiting for the partner of a
+ *     FIFO open (fifo_open), for a conflicting lease to break (__break_lease),
+ *     or inside a driver's open method. The wait returns -ERESTARTSYS, which
+ *     is restarted after the handler with SA_RESTART and reported as EINTR
+ *     otherwise.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: yes
+ *
+ * side-effect: resource_create | alloc_memory
+ *   target: file descriptor, file structure, dentry cache
+ *   desc: Allocates a new file descriptor in the process's fd table. Allocates
+ *     a struct file from the filp slab cache. May allocate dentries and inodes
+ *     during path lookup. System-wide file count (nr_files) is incremented.
+ *   reversible: yes
+ *
+ * side-effect: filesystem
+ *   target: filesystem, inode
+ *   condition: When O_CREAT is specified and file doesn't exist
+ *   desc: Creates a new file on the filesystem. Creates new inode, allocates
+ *     data blocks as needed, and creates directory entry. Updates parent
+ *     directory mtime and ctime.
+ *   reversible: no
+ *
+ * side-effect: filesystem
+ *   target: file content
+ *   condition: When O_TRUNC is specified for existing file
+ *   desc: Truncates the file to zero length, releasing data blocks. Updates
+ *     file mtime and ctime. May trigger notifications to lease holders.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: inode timestamps
+ *   condition: When a symlink is followed, or O_TRUNC or O_CREAT takes effect
+ *   desc: Opening does not update the atime of the opened file. Reads update it
+ *     later, and O_NOATIME only affects those reads. Following a symlink in
+ *     the path may update the symlink's atime, subject to the mount atime
+ *     options. O_TRUNC and file creation update mtime and ctime.
+ *
+ * capability: CAP_DAC_OVERRIDE
+ *   type: bypass_check
+ *   allows: Bypass file read, write, and execute permission checks
+ *   without: Standard DAC (discretionary access control) checks are applied
+ *   condition: Checked when file permission would otherwise deny access
+ *
+ * capability: CAP_DAC_READ_SEARCH
+ *   type: bypass_check
+ *   allows: Bypass read permission on files and search permission on directories
+ *   without: Must have read permission on file or search permission on directory
+ *   condition: Checked during path traversal and file open
+ *
+ * capability: CAP_FOWNER
+ *   type: bypass_check
+ *   allows: Use O_NOATIME on files not owned by caller
+ *   without: O_NOATIME returns EPERM if caller is not file owner
+ *   condition: Checked when O_NOATIME is specified and caller is not owner
+ *
+ * capability: CAP_SYS_ADMIN
+ *   type: increase_limit
+ *   allows: Exceed the system-wide file limit (file-max)
+ *   without: Returns ENFILE when system limit is reached
+ *   condition: Checked in alloc_empty_file() when nr_files >= max_files
+ *
+ * constraint: RLIMIT_NOFILE (per-process fd limit)
+ *   desc: The returned file descriptor must be less than the process's
+ *     RLIMIT_NOFILE limit. Default is typically 1024, maximum is controlled
+ *     by /proc/sys/fs/nr_open (default 1048576). Exceeding returns EMFILE.
+ *   expr: fd < rlimit(RLIMIT_NOFILE)
+ *
+ * constraint: file-max (system-wide limit)
+ *   desc: System-wide limit on open files in /proc/sys/fs/file-max. Processes
+ *     without CAP_SYS_ADMIN receive ENFILE when this limit is reached. The
+ *     limit is computed based on system memory at boot time.
+ *   expr: nr_files < files_stat.max_files || capable(CAP_SYS_ADMIN)
+ *
+ * constraint: PATH_MAX
+ *   desc: Maximum length of pathname including null terminator is PATH_MAX
+ *     (4096 bytes). Individual path components must not exceed NAME_MAX (255).
+ *
+ * examples: fd = open("/etc/passwd", O_RDONLY);  // Read existing file
+ *   fd = open("/tmp/newfile", O_WRONLY | O_CREAT | O_TRUNC, 0644);  // Create/truncate
+ *   fd = open("/tmp/lockfile", O_WRONLY | O_CREAT | O_EXCL, 0600);  // Exclusive create
+ *   fd = open("/dev/null", O_RDWR);  // Open device
+ *   fd = open("/tmp", O_RDONLY | O_DIRECTORY);  // Open directory
+ *   fd = open("/tmp", O_TMPFILE | O_RDWR, 0600);  // Anonymous temp file
+ *
+ * notes: O_RDONLY is defined as 0, so (flags & O_RDONLY) always evaluates to zero.
+ *   Test access mode using (flags & O_ACCMODE) == O_RDONLY.
+ *
+ *   When O_CREAT is specified without O_EXCL, there is a race condition between
+ *   testing for file existence and creating it. Use O_CREAT | O_EXCL for atomic
+ *   exclusive file creation.
+ *
+ *   O_CLOEXEC should be used in multithreaded programs to prevent file descriptor
+ *   leaks to child processes between fork() and execve().
+ *
+ *   O_DIRECT has alignment requirements that vary by filesystem. Use statx()
+ *   with STATX_DIOALIGN (Linux 6.1+) to query requirements. Unaligned I/O may
+ *   fail with EINVAL or fall back to buffered I/O.
+ *
+ *   O_PATH opens a file descriptor that can be used only for certain operations
+ *   (fstat, dup, fcntl, close, fchdir on directories, as dirfd for *at() calls).
+ *   I/O operations will fail with EBADF.
+ */
 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
 {
 	if (force_o_largefile())
@@ -1537,6 +1861,239 @@ int filp_close(struct file *filp, fl_owner_t id)
 }
 EXPORT_SYMBOL(filp_close);
 
+/**
+ * sys_close - Close a file descriptor
+ * @fd: The file descriptor to close
+ *
+ * long-desc: Terminates access to an open file descriptor, releasing the file
+ *   descriptor for reuse by subsequent open(), dup(), or similar syscalls.
+ *
+ *   Traditional POSIX advisory record locks held by the process on the
+ *   associated file are released when any of its fds for that inode is
+ *   closed, not only the last one. OFD locks and flock locks are associated
+ *   with the open file description and are only released when the last
+ *   reference to that open file description is dropped.
+ *
+ *   Closing a file descriptor drops one reference to its open file
+ *   description. Other descriptors (dup(), fork(), SCM_RIGHTS messages in
+ *   flight) and operations still running on the file, such as a concurrent
+ *   read(), also hold references. Only when the last reference is dropped are
+ *   the associated resources freed. If the file was previously unlinked, the
+ *   file itself is deleted when the last reference is dropped.
+ *
+ *   Except when close() fails with EBADF, the file descriptor is released even
+ *   when close() returns an error, because it is released before the flush
+ *   that may fail. POSIX leaves the state of the descriptor unspecified after
+ *   EINTR. Retrying close() after an error may close an unrelated file
+ *   descriptor that another thread has since been given.
+ *
+ *   Errors returned from close() come only from the file's ->flush() method,
+ *   called by filp_flush(). Errors from ->release() are not reported.
+ *   Filesystems without a ->flush() method, such as ext4, xfs and btrfs, never
+ *   report write errors from close(). Network filesystems such as NFS, CIFS and
+ *   FUSE implement ->flush() and report deferred write errors (EIO, ENOSPC,
+ *   EDQUOT) at close time. A successful return does not mean the data reached
+ *   storage; call fsync() before close() for that.
+ *
+ *   On close, the following cleanup operations are performed: the ->flush()
+ *   method is called if the file has one, POSIX advisory locks are removed,
+ *   dnotify registrations are cleaned up, and the file reference is released.
+ *   If this was the last reference, additional cleanup includes: fsnotify close
+ *   notification, epoll cleanup, OFD, flock and lease removal, FASYNC cleanup,
+ *   the ->release() method, and the file structure deallocation.
+ *
+ * contexts: process, sleepable
+ *
+ * param: fd
+ *   type: fd, input
+ *   constraint-type: range(0, INT_MAX)
+ *   cdesc: Must be a valid, open file descriptor for the current process.
+ *     The value 0, 1, or 2 (stdin, stdout, stderr) may be closed like any other
+ *     fd, though this is unusual and may cause issues with libraries that assume
+ *     these descriptors are valid. The parameter is unsigned int to match kernel
+ *     file descriptor table indexing. A value that is not open, including any
+ *     value at or above the current table size, fails with EBADF.
+ *
+ * return:
+ *   type: int
+ *   check-type: exact
+ *   success: 0
+ *   desc: Returns 0 on success. On error, returns a negative error code. Except
+ *     for EBADF, the file descriptor is still closed when an error is returned
+ *     and must not be used again. The error comes from the file's ->flush()
+ *     method, not from the fd remaining open. With EBADF, nothing was released.
+ *
+ * error: EBADF, Bad file descriptor
+ *   desc: fd is at or above the file descriptor table size, has no file
+ *     assigned (including a slot reserved by a concurrent open() that has not
+ *     installed its file yet), or was already closed. This is the only error
+ *     for which no file descriptor was released.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: The flush operation was interrupted by a signal before completion.
+ *     This occurs when a driver's ->flush() method (for example wdm_flush() in
+ *     drivers/usb/class/cdc-wdm.c) performs an interruptible wait that receives
+ *     a signal. The file descriptor is still released and must not be used
+ *     again. Kernel-internal restart codes (ERESTARTSYS,
+ *     ERESTARTNOINTR, ERESTARTNOHAND, ERESTART_RESTARTBLOCK) are converted to
+ *     EINTR because restarting the syscall would be incorrect once the fd is
+ *     freed.
+ *
+ * error: EIO, I/O error
+ *   desc: The file's ->flush() method reported an I/O error, typically a
+ *     deferred write error on a network filesystem such as NFS, CIFS or FUSE,
+ *     or an error from a driver's ->flush(). Previously buffered write data
+ *     may have been lost.
+ *
+ * error: ENOSPC, No space left on device
+ *   desc: The file's ->flush() method reported that there was insufficient
+ *     space to flush buffered writes, for example on NFS when the server runs
+ *     out of space between write() and close().
+ *
+ * error: EDQUOT, Disk quota exceeded
+ *   desc: The file's ->flush() method reported that the user's disk quota was
+ *     exceeded while flushing buffered writes, for example on NFS when the
+ *     quota is exceeded between write() and close().
+ *
+ * lock: files->file_lock
+ *   type: spinlock
+ *   acquired: true
+ *   released: true
+ *   desc: Taken by file_close_fd() to look up and clear the fd slot atomically,
+ *     so two concurrent close() calls on one fd cannot both obtain the struct
+ *     file. Dropped before ->flush() and the final fput. From then on the fd
+ *     number may be handed out again. An fd reserved by a concurrent open() but
+ *     not yet installed has a NULL slot, so close() returns EBADF.
+ *
+ * lock: file->f_lock
+ *   type: spinlock
+ *   acquired: true
+ *   released: true
+ *   desc: Taken from __fput() on the last reference, by eventpoll_release_file()
+ *     when the file is registered with epoll, and by fasync_remove_entry()
+ *     (through the ->fasync() method) when FASYNC is set. Protects the epoll
+ *     and fasync links of the file.
+ *
+ * lock: ep->mtx
+ *   type: mutex
+ *   acquired: true
+ *   released: true
+ *   desc: Acquired during epoll cleanup if the file was monitored by epoll.
+ *     Used to safely remove the file from epoll interest lists.
+ *
+ * lock: flc_lock
+ *   type: spinlock
+ *   acquired: true
+ *   released: true
+ *   desc: File lock context spinlock. Taken by locks_remove_posix() from
+ *     filp_flush() on every close when the file has POSIX locks, and by
+ *     locks_remove_file() from __fput() on the last reference to remove OFD,
+ *     flock, and lease locks.
+ *
+ * signal: pending_signals
+ *   direction: receive
+ *   action: return
+ *   condition: When close-time flush performs interruptible wait
+ *   desc: If the close-time ->flush() method (for example wdm_flush() in
+ *     cdc-wdm) performs an interruptible wait and a signal is pending, the wait
+ *     is interrupted. Any kernel restart codes are converted to EINTR since
+ *     close cannot be restarted after the fd is freed.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: no
+ *
+ * side-effect: resource_destroy | irreversible
+ *   target: File descriptor table entry
+ *   desc: The file descriptor is removed from the process's file descriptor
+ *     table, making the fd number available for reuse by subsequent open(),
+ *     dup(), or similar calls. This happens before the flush that may fail, so
+ *     an error return does not undo it.
+ *   condition: Always (when fd is valid)
+ *   reversible: no
+ *
+ * side-effect: lock_release
+ *   target: POSIX advisory locks, OFD locks, flock locks
+ *   desc: POSIX locks held by this process on the inode are removed on every
+ *     close via locks_remove_posix() in filp_flush(), except for O_PATH files.
+ *     OFD and flock locks are removed via locks_remove_file() in __fput() only
+ *     when this is the last reference to the open file description.
+ *   condition: Any close of a non-O_PATH file for POSIX locks, last reference
+ *     for OFD and flock locks
+ *   reversible: no
+ *
+ * side-effect: resource_destroy
+ *   target: File leases
+ *   desc: Any file leases held on the file are removed during locks_remove_file()
+ *     when this is the last reference to the open file description.
+ *   condition: File had leases and this is the last reference
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: dnotify registrations
+ *   desc: Directory notification (dnotify) registrations associated with this
+ *     file are cleaned up via dnotify_flush(). This only applies to directories.
+ *   condition: File is a directory with dnotify registrations
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: epoll interest lists
+ *   desc: If the file was being monitored by epoll instances, it is removed
+ *     from those interest lists via eventpoll_release(), which runs from
+ *     __fput() only on the last reference. While other references remain, the
+ *     epoll registrations stay active.
+ *   condition: File was added to epoll instances and this is the last reference
+ *   reversible: no
+ *
+ * side-effect: filesystem
+ *   target: Buffered data
+ *   desc: The file's ->flush() method runs before the file reference is
+ *     dropped (for example on NFS, CIFS and FUSE) and may return errors such as
+ *     EIO, ENOSPC or EDQUOT.
+ *   condition: The filesystem or driver provides a ->flush() method
+ *   reversible: no
+ *
+ * side-effect: free_memory
+ *   target: struct file and related structures
+ *   desc: When this is the last reference to the file, the file structure is
+ *     freed and the dentry and mount references are released.
+ *   condition: This is the last reference to the file
+ *   reversible: no
+ *
+ * side-effect: filesystem
+ *   target: Unlinked file deletion
+ *   desc: If the file was previously unlinked (deleted) but kept open, closing
+ *     the last reference causes the actual file data to be removed from the
+ *     filesystem and the inode to be freed.
+ *   condition: File was unlinked and this is the last reference
+ *   reversible: no
+ *
+ * state-trans: file_descriptor
+ *   from: open
+ *   to: closed/free
+ *   condition: Valid fd passed to close
+ *   desc: The file descriptor transitions from open (usable) to closed (invalid).
+ *     The fd number becomes available for reuse.
+ *
+ * state-trans: file_reference_count
+ *   from: n
+ *   to: n-1 (or freed if n was 1)
+ *   condition: Always on successful fd lookup
+ *   desc: The file's reference count is decremented. If this was the last
+ *     reference, the file is fully cleaned up and freed.
+ *
+ * examples: close(fd);  // Ignoring the result loses ->flush() errors
+ *   if (close(fd) == -1) perror("close");  // Log errors for debugging
+ *   fsync(fd); close(fd);  // Ensure data persistence before closing
+ *
+ * notes: close() drops its reference with fput_close_sync(), so when it is the last
+ *   reference __fput() runs synchronously in the calling task before close()
+ *   returns, instead of being deferred to task work.
+ *
+ *   Calling close() on a file descriptor while another thread is using it
+ *   (e.g., in a blocking read() or write()) does not interrupt the blocked
+ *   operation. The blocked operation continues on the underlying file and
+ *   may complete even after close() returns.
+ */
 /*
  * Careful here! We test whether the file pointer is NULL before
  * releasing the fd. This ensures that one clone task can't release
diff --git a/fs/read_write.c b/fs/read_write.c
index e8c14e2760b20..a9c07c21f5888 100644
--- a/fs/read_write.c
+++ b/fs/read_write.c
@@ -720,6 +720,303 @@ ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
 	return ret;
 }
 
+/**
+ * sys_read - Read data from a file descriptor
+ * @fd: File descriptor to read from
+ * @buf: User-space buffer to read data into
+ * @count: Maximum number of bytes to read
+ *
+ * long-desc: Reads at most count bytes from fd into the user buffer buf. For
+ *   seekable files (regular files, block devices), the read begins at the
+ *   current file offset, and the file offset is advanced by the number of
+ *   bytes read. For stream files (FMODE_STREAM, such as pipes, FIFOs, and
+ *   sockets), the file offset is not used. Other files, including many
+ *   character devices, keep a file offset although a driver may ignore it.
+ *
+ *   If count is zero, the usual checks still run and the file's read method is
+ *   still called. Common implementations (regular files, pipes, sockets)
+ *   return zero without transferring data.
+ *
+ *   On success the number of bytes read is returned; zero means end of file
+ *   for regular files. A read can return fewer bytes than requested, for
+ *   example near end of file, on a pipe, socket or terminal with less data
+ *   pending, or when a signal arrives after some data was copied.
+ *
+ *   On Linux, read() transfers at most MAX_RW_COUNT (INT_MAX & PAGE_MASK,
+ *   0x7ffff000 with 4KB pages, just under 2GB) bytes per call, regardless of
+ *   whether the filesystem would allow more. This is to avoid issues with
+ *   signed arithmetic overflow on 32-bit systems.
+ *
+ *   POSIX allows reads that are interrupted after reading some data to either
+ *   return -1 (with errno set to EINTR) or return the number of bytes already
+ *   read. Linux follows the latter behavior: if data has been read before a
+ *   signal arrives, the call returns the bytes read rather than failing.
+ *
+ * contexts: process, sleepable
+ *
+ * param: fd
+ *   type: fd, input
+ *   constraint-type: range(0, INT_MAX)
+ *   cdesc: Must be a valid, open file descriptor with read permission.
+ *     The file must have been opened with O_RDONLY or O_RDWR. Special values
+ *     like AT_FDCWD are not valid. File descriptors for directories return
+ *     EISDIR. Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are
+ *     valid if open and readable.
+ *
+ * param: buf
+ *   type: user_ptr, output
+ *   constraint-type: buffer(2)
+ *   cdesc: Must point to a writable user-space region of at least count bytes.
+ *     The range is checked via access_ok() and a range outside user space
+ *     fails with EFAULT. NULL is not rejected by that check. With a count of 0
+ *     the call returns 0 in common cases. With a count above 0 the copy fails
+ *     with EFAULT, or the call returns a short count if some data was copied
+ *     first. The buffer may be partially written if an error occurs mid-read.
+ *     O_DIRECT reads may require block-size alignment (see STATX_DIOALIGN).
+ *
+ * param: count
+ *   type: uint, input
+ *   cdesc: Maximum number of bytes to read. The range buf to buf + count must
+ *     lie within user space or access_ok() fails with EFAULT, which includes
+ *     counts that do not fit in ssize_t on 64-bit kernels. A count of 0 passes
+ *     the same checks and the file's read method is still called, typically
+ *     returning 0 without transferring data. After access_ok() and
+ *     rw_verify_area() succeed, counts above MAX_RW_COUNT (INT_MAX &
+ *     PAGE_MASK, 0x7ffff000 with 4KB pages) are clamped to MAX_RW_COUNT.
+ *
+ * return:
+ *   type: int
+ *   check-type: range
+ *   success: >= 0
+ *   desc: On success, returns the number of bytes read (non-negative). Zero
+ *     indicates end-of-file (EOF) for regular files, or no data available
+ *     from a device that does not block. The return value may be less than
+ *     count if fewer bytes were available (short read). Partial reads are
+ *     not errors. On error, returns a negative error code.
+ *
+ * error: EBADF, Bad file descriptor
+ *   desc: fd is not a valid file descriptor, or fd was not opened for reading.
+ *     This includes file descriptors opened with O_WRONLY, O_PATH, or file
+ *     descriptors that have been closed. Also returned if the file structure
+ *     does not have FMODE_READ set.
+ *
+ * error: EFAULT, Bad address
+ *   desc: buf points outside the accessible address space. The buffer address
+ *     failed access_ok() validation. Can also occur if a fault happens during
+ *     copy_to_user() when transferring data to user space after the read
+ *     completes in kernel space.
+ *
+ * error: EINVAL, Invalid argument
+ *   desc: Returned in several cases: (1) The file has no read or read_iter
+ *     method (FMODE_CAN_READ is not set). (2) The file was opened with
+ *     O_DIRECT and the buffer alignment, offset, or count does not meet the
+ *     filesystem's alignment requirements. (3) For timerfd file descriptors,
+ *     the buffer is smaller than 8 bytes. (4) rw_verify_area() rejects a
+ *     negative file position, or a position plus count that overflows, on a
+ *     file without FOP_UNSIGNED_OFFSET. (5) The count, cast to ssize_t, is
+ *     negative (32-bit kernels only).
+ *
+ * error: EISDIR, Is a directory
+ *   desc: fd refers to a directory. Directories cannot be read using read();
+ *     use getdents64() instead. This error is returned by the generic_read_dir()
+ *     handler installed for directory file operations.
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ *   desc: fd refers to a file (pipe, socket, device) that is marked non-blocking
+ *     (O_NONBLOCK) and the read would block. Equivalent to EWOULDBLOCK. The
+ *     application should retry the read later or use select/poll/epoll.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: The call was interrupted by a signal before any data was read. This
+ *     only occurs if no data has been transferred; if some data was read before
+ *     the signal, the call returns the number of bytes read. The caller should
+ *     typically restart the read.
+ *
+ * error: EIO, Input/output error
+ *   desc: A low-level I/O error occurred. For regular files, this typically
+ *     indicates a hardware error on the storage device, a filesystem error,
+ *     or a network filesystem timeout. For terminals, it is returned when a
+ *     background process group reads its controlling terminal and SIGTTIN is
+ *     ignored or blocked, or the process group is orphaned.
+ *
+ * error: EOVERFLOW, Value too large for defined data type
+ *   desc: Returned by rw_verify_area() only for files with FOP_UNSIGNED_OFFSET
+ *     (for example /proc/pid/mem) when the file position is negative, meaning
+ *     above LLONG_MAX, and count is at least -pos so that the read would wrap
+ *     past the end of the 64-bit offset space. Files without
+ *     FOP_UNSIGNED_OFFSET get EINVAL for a negative position or a position
+ *     plus count that overflows.
+ *
+ * error: ENOBUFS, No buffer space available
+ *   desc: Returned when reading from pipe-based watch queues (CONFIG_WATCH_QUEUE)
+ *     when the buffer is too small to hold a complete notification, or when
+ *     reading packets from pipes with PIPE_BUF_FLAG_WHOLE set.
+ *
+ * error: ERESTARTSYS, Restart system call (internal)
+ *   desc: Internal error code indicating the syscall should be restarted. This
+ *     is typically translated to EINTR if SA_RESTART is not set on the signal
+ *     handler, or the syscall is transparently restarted if SA_RESTART is set.
+ *     User space should not see this error code directly.
+ *
+ * error: EACCES, Permission denied
+ *   desc: The security subsystem (LSM such as SELinux or AppArmor) denied
+ *     the read operation via security_file_permission(). This can occur even
+ *     if the file was successfully opened, as LSM policies may enforce checks
+ *     on each operation.
+ *
+ * error: EPERM, Operation not permitted
+ *   desc: Returned by fanotify permission events (CONFIG_FANOTIFY_ACCESS_PERMISSIONS)
+ *     when a user-space fanotify listener denies the read operation via
+ *     fsnotify_file_area_perm().
+ *
+ * error: ENODATA, No data available
+ *   desc: Returned when a filesystem built on netfs (for example AFS or Ceph)
+ *     ends a read subrequest short without making progress.
+ *
+ * error: EOPNOTSUPP, Operation not supported
+ *   desc: Returned by the read method of the file, not by the generic VFS
+ *     path. For example, a socket whose protocol has no receive method
+ *     (sock_no_recvmsg) fails with EOPNOTSUPP. A file with no read method at
+ *     all fails with EINVAL instead.
+ *
+ * lock: file->f_pos_lock
+ *   type: mutex
+ *   acquired: true
+ *   released: true
+ *   desc: For regular files that require atomic position updates (FMODE_ATOMIC_POS),
+ *     the f_pos_lock mutex is acquired by fdget_pos() at syscall entry and released
+ *     by fdput_pos() at syscall exit. This serializes concurrent reads that share
+ *     the same file description. Not acquired for files opened with FMODE_STREAM
+ *     (pipes, sockets) or when the file is not shared.
+ *
+ * lock: Filesystem-specific locks
+ *   type: custom
+ *   acquired: true
+ *   released: true
+ *   desc: The filesystem's read_iter or read method may acquire additional locks.
+ *     For regular files, this typically includes the inode's i_rwsem for certain
+ *     operations. For pipes, the pipe->mutex is acquired. For sockets, socket
+ *     lock is acquired. These are internal to the file operation and released
+ *     before return.
+ *
+ * lock: RCU read-side
+ *   type: rcu
+ *   acquired: true
+ *   released: true
+ *   desc: Taken only when the files_struct is shared with other threads, in
+ *     which case the fd lookup in fdget() uses RCU and takes a file reference.
+ *     A private table needs no RCU. The RCU read lock is acquired and released
+ *     internally by the fd lookup path, not held across the entire syscall.
+ *     fdput() releases the file reference count, not the RCU lock.
+ *
+ * signal: Any signal
+ *   direction: receive
+ *   action: return
+ *   condition: When blocked waiting for data on interruptible operations
+ *   desc: The syscall may be interrupted by signals while waiting for data to
+ *     become available (pipes, sockets, terminals). If interrupted before any
+ *     data is read, returns -EINTR or -ERESTARTSYS. If data has already been
+ *     read, returns the number of bytes read.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: yes
+ *
+ * side-effect: file_position
+ *   target: file->f_pos
+ *   condition: For seekable files when read succeeds (returns > 0)
+ *   desc: The file offset (f_pos) is advanced by the number of bytes read.
+ *     For stream files (FMODE_STREAM such as pipes and sockets), the offset
+ *     is not used or modified. The offset update is protected by f_pos_lock
+ *     when the file is shared between threads/processes.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: inode access time (atime)
+ *   condition: When the read_iter implementation calls file_accessed() and
+ *     O_NOATIME is not set
+ *   desc: The filesystem's read_iter implementation updates atime via
+ *     file_accessed() and touch_atime(). Buffered reads (filemap_read) do this
+ *     after the data loop, including at EOF and after errors, but not for a
+ *     count of 0. O_DIRECT reads (generic_file_read_iter) do it before the
+ *     I/O. The update may be suppressed by mount options (noatime, relatime),
+ *     the O_NOATIME flag, or if the filesystem does not support atime.
+ *     Relatime updates atime only if it is older than mtime or ctime, or more
+ *     than a day old.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: task I/O accounting
+ *   condition: When CONFIG_TASK_XACCT is enabled and the read method is invoked
+ *   desc: Updates the current task's I/O accounting statistics. The rchar field
+ *     (read characters) is incremented by bytes read via add_rchar() only on
+ *     successful reads (ret > 0). The syscr field (syscall read count) is
+ *     incremented via inc_syscr() once the read method has been invoked, even
+ *     if it fails, but not when the FMODE_READ, FMODE_CAN_READ, access_ok() or
+ *     rw_verify_area() checks fail. These statistics are visible in
+ *     /proc/[pid]/io.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: fsnotify events
+ *   condition: When read returns > 0
+ *   desc: Generates an FS_ACCESS fsnotify event via fsnotify_access() allowing
+ *     inotify, fanotify, and dnotify watchers to be notified of the read. This
+ *     occurs after data transfer completes successfully.
+ *   reversible: no
+ *
+ * constraint: MAX_RW_COUNT
+ *   desc: After the access_ok() and rw_verify_area() checks pass, the count
+ *     parameter is silently clamped to MAX_RW_COUNT (INT_MAX & PAGE_MASK, just
+ *     under 2GB) to prevent integer overflow in internal calculations. This is
+ *     transparent to the caller. The syscall succeeds but reads at most
+ *     MAX_RW_COUNT bytes.
+ *   expr: actual_count = min(count, MAX_RW_COUNT)
+ *
+ * constraint: File must be open for reading
+ *   desc: The file descriptor must have been opened with O_RDONLY or O_RDWR.
+ *     Files opened with O_WRONLY or O_PATH lack FMODE_READ and return EBADF.
+ *     Files that lack FMODE_CAN_READ because they have no read or read_iter
+ *     method return EINVAL.
+ *   expr: (file->f_mode & FMODE_READ) && (file->f_mode & FMODE_CAN_READ)
+ *
+ * examples: n = read(fd, buf, sizeof(buf));  // Basic read
+ *   n = read(STDIN_FILENO, buf, 1024);  // Read from stdin
+ *   while ((n = read(fd, buf, 4096)) > 0) { process(buf, n); }  // Read loop
+ *   if (read(fd, buf, count) == 0) { handle_eof(); }  // Check for EOF
+ *
+ * notes: The behavior of read() varies significantly depending on the type of
+ *   file descriptor:
+ *
+ *   - Regular files: Reads from current position, advances position, returns 0
+ *     at EOF. Short reads are rare but possible near EOF or on signal.
+ *
+ *   - Pipes and FIFOs: Blocking by default. Returns available data (up to count)
+ *     or blocks until data is available. Returns 0 when all writers have closed.
+ *     O_NONBLOCK returns EAGAIN when empty instead of blocking.
+ *
+ *   - Sockets: Similar to pipes. Specific behavior depends on socket type and
+ *     protocol. MSG_* flags can be specified via recv() for more control.
+ *
+ *   - Terminals: Line-buffered in canonical mode; read returns when newline is
+ *     entered or buffer is full. Raw mode returns immediately when data available.
+ *     Special handling for signals (SIGINT on Ctrl+C, etc.).
+ *
+ *   - Device special files: Behavior is device-specific. Some devices support
+ *     seeking, others do not. Read size may be constrained by device.
+ *
+ *   Race condition: Concurrent reads from the same file description (not just
+ *   file descriptor) can race on the file position. Linux 3.14+ provides atomic
+ *   position updates for regular files via f_pos_lock, but applications should
+ *   use pread() for concurrent positioned reads.
+ *
+ *   O_DIRECT reads bypass the page cache and typically require aligned buffers
+ *   and positions. Alignment requirements are filesystem-specific; use statx()
+ *   with STATX_DIOALIGN (Linux 6.1+) to query. Unaligned O_DIRECT reads fail
+ *   with EINVAL on most filesystems.
+ *
+ *   For splice(2)-like zero-copy reads, consider using splice(), sendfile(),
+ *   or copy_file_range() instead of read() + write().
+ */
 SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
 {
 	return ksys_read(fd, buf, count);
@@ -744,6 +1041,419 @@ ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
 	return ret;
 }
 
+/**
+ * sys_write - Write data to a file descriptor
+ * @fd: File descriptor to write to
+ * @buf: User-space buffer containing data to write
+ * @count: Maximum number of bytes to write
+ *
+ * long-desc: Writes at most count bytes from the user buffer buf to fd. For
+ *   seekable files (regular files, block devices), the write begins at the
+ *   current file offset, and the file offset is advanced by the number of
+ *   bytes written. If the file was opened with O_APPEND, the file offset is
+ *   first set to the end of the file before writing. For stream files
+ *   (FMODE_STREAM, such as pipes, FIFOs, and sockets), the file offset is not
+ *   used and writing occurs at the position defined by the device. Other
+ *   files, including many character devices, keep a file offset although a
+ *   driver may ignore it.
+ *
+ *   Fewer than count bytes may be written, for example when the filesystem
+ *   runs out of space, the write reaches RLIMIT_FSIZE, or a signal arrives
+ *   after some data was written. The caller writes the rest with another
+ *   write() call.
+ *
+ *   On Linux, write() transfers at most MAX_RW_COUNT (INT_MAX & PAGE_MASK,
+ *   0x7ffff000 with 4KB pages, just under 2GB) bytes per call, regardless of
+ *   whether the file or filesystem would allow more. This prevents signed
+ *   arithmetic overflow.
+ *
+ *   For regular files, a successful write() does not guarantee that data has been
+ *   committed to disk. Use fsync(2) or fdatasync(2) if durability is required.
+ *   For O_SYNC or O_DSYNC files, the kernel automatically syncs data on write.
+ *
+ *   POSIX permits writes that are interrupted after partial writes to either
+ *   return -1 with errno=EINTR, or to return the count of bytes already written.
+ *   Linux implements the latter behavior: if some data has been written before
+ *   a signal arrives, write() returns the number of bytes written rather than
+ *   failing with EINTR.
+ *
+ * contexts: process, sleepable
+ *
+ * param: fd
+ *   type: fd, input
+ *   constraint-type: range(0, INT_MAX)
+ *   cdesc: Must be a valid, open file descriptor with write permission.
+ *     The file must have been opened with O_WRONLY or O_RDWR. File descriptors
+ *     opened with O_RDONLY, O_PATH, or that have been closed return EBADF.
+ *     Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are valid if
+ *     open and writable. AT_FDCWD and other special values are not valid.
+ *
+ * param: buf
+ *   type: user_ptr, input
+ *   constraint-type: buffer(2)
+ *   cdesc: Must point to a readable user-space region of at least count bytes.
+ *     The range is checked via access_ok() and a range outside user space
+ *     fails with EFAULT. NULL is not rejected by that check. With a count of 0
+ *     the call returns 0 in common cases. With a count above 0 the copy from
+ *     user space fails with EFAULT, or the call returns a short count if part
+ *     of the buffer was copied first. O_DIRECT writes may require block-size
+ *     alignment (see STATX_DIOALIGN).
+ *
+ * param: count
+ *   type: uint, input
+ *   cdesc: Maximum number of bytes to write. The range buf to buf + count must
+ *     lie within user space or access_ok() fails with EFAULT, which includes
+ *     counts that do not fit in ssize_t on 64-bit kernels. On 32-bit kernels
+ *     such a count reaches rw_verify_area() and fails with EINVAL. A count of
+ *     0 is passed to the file's write method, which typically returns 0 but
+ *     may trigger driver-specific side effects. After the checks pass, counts
+ *     above MAX_RW_COUNT (INT_MAX & PAGE_MASK, 0x7ffff000 with 4KB pages) are
+ *     clamped.
+ *
+ * return:
+ *   type: int
+ *   check-type: range
+ *   success: >= 0
+ *   desc: On success, returns the number of bytes written (non-negative). Zero
+ *     indicates that nothing was written (count was 0, or a device-specific
+ *     write method accepted no data). Non-blocking writes that cannot proceed
+ *     return -EAGAIN instead. The return value may be less than count due to
+ *     resource limits, signal interruption, or device constraints (short
+ *     write). On error, returns a negative error code.
+ *
+ * error: EBADF, Bad file descriptor
+ *   desc: fd is not a valid file descriptor, or fd was not opened for writing.
+ *     This includes file descriptors opened with O_RDONLY, O_PATH, or file
+ *     descriptors that have been closed. Also returned if the file structure
+ *     does not have FMODE_WRITE set.
+ *
+ * error: EFAULT, Bad address
+ *   desc: buf points outside the accessible address space. The buffer address
+ *     failed access_ok() validation. Can also occur if a fault happens during
+ *     copy_from_user() when reading data from user space.
+ *
+ * error: EINVAL, Invalid argument
+ *   desc: Returned in several cases: (1) The file has no write or write_iter
+ *     method (FMODE_CAN_WRITE is not set). (2) The file was opened with
+ *     O_DIRECT and the buffer alignment, offset, or count does not meet the
+ *     filesystem's alignment requirements. (3) rw_verify_area() rejects a
+ *     negative file position, or a position plus count that overflows, on a
+ *     file without FOP_UNSIGNED_OFFSET. (4) The count, cast to ssize_t, is
+ *     negative (32-bit kernels only, as 64-bit kernels fail access_ok() first
+ *     with EFAULT).
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ *   desc: fd refers to a file (pipe, socket, device) that is marked non-blocking
+ *     (O_NONBLOCK) and the write would block because the buffer is full.
+ *     Equivalent to EWOULDBLOCK. The application should retry later or use
+ *     select/poll/epoll to wait for writability.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: The call was interrupted by a signal before any data was written. This
+ *     only occurs if no data has been transferred; if some data was written
+ *     before the signal, the call returns the number of bytes written. The
+ *     caller should typically restart the write.
+ *
+ * error: EPIPE, Broken pipe
+ *   desc: fd refers to a pipe or socket whose reading end has been closed.
+ *     When this condition occurs, the calling process also receives a SIGPIPE
+ *     signal. If the signal is caught or ignored, EPIPE is still returned.
+ *     For sockets, MSG_NOSIGNAL (via send()) suppresses the signal. For
+ *     pwritev2(), the RWF_NOSIGNAL flag suppresses it.
+ *
+ * error: EFBIG, File too large
+ *   desc: The write starts at or beyond a file size limit. The generic write
+ *     path returns EFBIG when the starting position is at or beyond
+ *     RLIMIT_FSIZE, in which case the process also receives SIGXFSZ, or at or
+ *     beyond the maximum file size of the filesystem. Without O_LARGEFILE
+ *     (32-bit systems only) that maximum is 2GB minus one byte. A write that
+ *     starts below a limit but would cross it is shortened to end at the
+ *     limit.
+ *
+ * error: ENOSPC, No space left on device
+ *   desc: The device containing the file has no room for the data. This can
+ *     occur mid-write resulting in a short write followed by ENOSPC on retry.
+ *
+ * error: EDQUOT, Disk quota exceeded
+ *   desc: The user's quota of disk blocks on the filesystem has been exhausted.
+ *     Like ENOSPC, this can result in a short write.
+ *
+ * error: EIO, Input/output error
+ *   desc: A low-level I/O error occurred while modifying the inode or writing
+ *     data. This typically indicates hardware failure, filesystem corruption,
+ *     or network filesystem timeout. Some data may have been written.
+ *
+ * error: EPERM, Operation not permitted
+ *   desc: The operation was prevented (1) by a file seal (F_SEAL_WRITE or
+ *     F_SEAL_FUTURE_WRITE on memfd/shmem, or F_SEAL_GROW for a write that
+ *     extends the file), (2) by a filesystem-specific check, for example ext4
+ *     refuses writes to an immutable inode, (3) because the file is a
+ *     read-only block device, (4) by an LSM hook denying the operation, or (5)
+ *     by a fanotify pre-content (FAN_PRE_ACCESS) listener denying the write.
+ *
+ * error: EOVERFLOW, Value too large for defined data type
+ *   desc: Returned by rw_verify_area() only for files with FOP_UNSIGNED_OFFSET
+ *     (for example /proc/pid/mem) when the file position is negative, meaning
+ *     above LLONG_MAX, and count is at least -pos so that the write would wrap
+ *     past the end of the 64-bit offset space. Files without
+ *     FOP_UNSIGNED_OFFSET get EINVAL for a negative position or a position
+ *     plus count that overflows. Exceeding filesystem file size limits is
+ *     EFBIG, not EOVERFLOW.
+ *
+ * error: EDESTADDRREQ, Destination address required
+ *   desc: fd is a datagram socket for which no peer address has been set using
+ *     connect(2). Use sendto(2) to specify the destination address.
+ *
+ * error: ETXTBSY, Text file busy
+ *   desc: The file is being used as a swap file (IS_SWAPFILE).
+ *
+ * error: EXDEV, Cross-device link
+ *   desc: When writing to a pipe that has been configured as a watch queue
+ *     (CONFIG_WATCH_QUEUE), direct write() calls are not supported.
+ *
+ * error: ENOMEM, Out of memory
+ *   desc: Insufficient kernel memory was available for the write operation.
+ *     For pipes, this occurs when allocating pages for the pipe buffer.
+ *
+ * error: ERESTARTSYS, Restart system call (internal)
+ *   desc: Internal error code indicating the syscall should be restarted. This
+ *     is converted to EINTR if SA_RESTART is not set on the signal handler, or
+ *     the syscall is transparently restarted if SA_RESTART is set. User space
+ *     should not see this error code directly.
+ *
+ * error: EACCES, Permission denied
+ *   desc: The security subsystem (LSM such as SELinux or AppArmor) denied the
+ *     write operation via security_file_permission(). This can occur even if
+ *     the file was successfully opened.
+ *
+ * lock: file->f_pos_lock
+ *   type: mutex
+ *   acquired: true
+ *   released: true
+ *   desc: For regular files that require atomic position updates (FMODE_ATOMIC_POS),
+ *     the f_pos_lock mutex is acquired by fdget_pos() at syscall entry and released
+ *     by fdput_pos() at syscall exit. This serializes concurrent writes sharing
+ *     the same file description. Not acquired for stream files (FMODE_STREAM like
+ *     pipes and sockets) or when the file is not shared.
+ *
+ * lock: sb->s_writers (freeze protection)
+ *   type: custom
+ *   acquired: true
+ *   released: true
+ *   desc: For regular files, file_start_write() acquires freeze protection on
+ *     the superblock via sb_start_write() before the write, and file_end_write()
+ *     releases it after. This prevents writes during filesystem freeze. Not
+ *     acquired for non-regular files (pipes, sockets, devices).
+ *
+ * lock: inode->i_rwsem
+ *   type: semaphore
+ *   acquired: true
+ *   released: true
+ *   desc: For regular files using generic_file_write_iter(), the inode's i_rwsem
+ *     is acquired in write mode before modifying file data. This is internal to
+ *     the filesystem and released before return. Not all filesystems use this
+ *     pattern.
+ *
+ * lock: pipe->mutex
+ *   type: mutex
+ *   acquired: true
+ *   released: true
+ *   desc: For pipes and FIFOs, the pipe's mutex is held while modifying pipe
+ *     buffers. Released temporarily while waiting for space, then reacquired.
+ *
+ * lock: RCU read-side
+ *   type: rcu
+ *   acquired: true
+ *   released: true
+ *   desc: Taken only when the files_struct is shared with other threads, in
+ *     which case the fd lookup in fdget() uses RCU and takes a file reference.
+ *     A private table needs no RCU. The RCU read lock is acquired and released
+ *     internally by the fd lookup path, not held across the entire syscall.
+ *     fdput() releases the file reference count, not the RCU lock.
+ *
+ * signal: SIGPIPE
+ *   number: SIGPIPE
+ *   direction: send
+ *   action: terminate
+ *   condition: Writing to a pipe or socket with no readers
+ *   desc: When writing to a pipe whose read end is closed, or a socket whose
+ *     peer has closed, SIGPIPE is sent to the calling process. The default
+ *     action terminates the process. Use signal(SIGPIPE, SIG_IGN) to suppress
+ *     for write(). EPIPE is returned regardless of signal disposition.
+ *   timing: during
+ *
+ * signal: SIGXFSZ
+ *   number: SIGXFSZ
+ *   direction: send
+ *   action: coredump
+ *   condition: Write starts at or beyond RLIMIT_FSIZE
+ *   desc: When a write starts at or beyond the soft file size limit
+ *     (RLIMIT_FSIZE), generic_write_check_limits() sends SIGXFSZ and the write
+ *     returns EFBIG. The default action terminates with a core dump. A write
+ *     that starts below the limit but would cross it is shortened to end at
+ *     the limit, with no signal. If RLIMIT_FSIZE is RLIM_INFINITY, no signal
+ *     is sent.
+ *   timing: during
+ *
+ * signal: Any signal
+ *   direction: receive
+ *   action: return
+ *   condition: While blocked waiting for space (pipes, sockets)
+ *   desc: The syscall may be interrupted by signals while waiting for buffer
+ *     space to become available. If interrupted before any data is written,
+ *     returns -EINTR or -ERESTARTSYS. If data was already written, returns the
+ *     byte count. Restartable if SA_RESTART is set and no data was written.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: yes
+ *
+ * side-effect: file_position
+ *   target: file->f_pos
+ *   condition: For seekable files when write succeeds (returns > 0)
+ *   desc: The file offset (f_pos) is advanced by the number of bytes written.
+ *     For files opened with O_APPEND, f_pos is first set to file size. For
+ *     stream files (FMODE_STREAM such as pipes and sockets), the offset is not
+ *     used or modified. Position updates are protected by f_pos_lock when
+ *     shared.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: inode timestamps (mtime, ctime)
+ *   condition: Before the data is copied, for a non-zero count on the generic
+ *     write path
+ *   desc: Updates the file's modification time (mtime) and change time (ctime)
+ *     via file_update_time(), which runs before the data is copied, so the
+ *     timestamps can change even if the write then fails or is short. The
+ *     update is skipped for inodes flagged NOCMTIME and when the timestamps
+ *     are unchanged. The timestamp precision depends on whether the filesystem
+ *     type sets FS_MGTIME (multigrain timestamps).
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: SUID/SGID bits (mode) and file capabilities
+ *   condition: Write to a regular file that has S_ISUID, S_ISGID, or file
+ *     capabilities set, with a non-zero count
+ *   desc: file_remove_privs() clears S_ISUID, and clears S_ISGID when S_IXGRP
+ *     is also set or the caller is not in the file's group, unless the caller
+ *     has CAP_FSETID. File capabilities (the security.capability xattr) are
+ *     removed as well, regardless of CAP_FSETID. This is a security feature to
+ *     prevent privilege escalation via modified setuid binaries. It runs before
+ *     the data is copied.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: file data
+ *   condition: When write succeeds (returns > 0)
+ *   desc: Modifies the file's data content. For regular files, data is written
+ *     to the page cache (buffered I/O) or directly to storage (O_DIRECT).
+ *     Data is not guaranteed to be persistent until fsync() or fdatasync()
+ *     completes, or writeback has written it to storage.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: task I/O accounting
+ *   condition: When CONFIG_TASK_XACCT is enabled and the write method is
+ *     invoked
+ *   desc: Updates the current task's I/O accounting statistics. The wchar field
+ *     (write characters) is incremented by bytes written via add_wchar() only on
+ *     successful writes (ret > 0). The syscw field (syscall write count) is
+ *     incremented via inc_syscw() once the write method has been invoked, even
+ *     if it fails, but not when the FMODE_WRITE, FMODE_CAN_WRITE, access_ok()
+ *     or rw_verify_area() checks fail. These statistics are visible in
+ *     /proc/[pid]/io.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: fsnotify events
+ *   condition: When write returns > 0
+ *   desc: Generates an FS_MODIFY fsnotify event via fsnotify_modify(), allowing
+ *     inotify, fanotify, and dnotify watchers to be notified of the write.
+ *
+ * capability: CAP_FSETID
+ *   type: bypass_check
+ *   allows: Keep the SUID and SGID bits set when the file is written
+ *   without: SUID is cleared on write, and SGID is cleared if S_IXGRP is set
+ *     or the caller is not in the file's group
+ *   condition: Checked in setattr_should_drop_suidgid() during
+ *     file_remove_privs()
+ *
+ * constraint: MAX_RW_COUNT
+ *   desc: After the access_ok() and rw_verify_area() checks pass, the count
+ *     parameter is silently clamped to MAX_RW_COUNT (INT_MAX & PAGE_MASK, just
+ *     under 2GB) to prevent integer overflow in internal calculations. This is
+ *     transparent to the caller.
+ *   expr: actual_count = min(count, MAX_RW_COUNT)
+ *
+ * constraint: File must be open for writing
+ *   desc: The file descriptor must have been opened with O_WRONLY or O_RDWR.
+ *     Files opened with O_RDONLY or O_PATH cannot be written and return EBADF.
+ *     The file must have both FMODE_WRITE and FMODE_CAN_WRITE flags set.
+ *   expr: (file->f_mode & FMODE_WRITE) && (file->f_mode & FMODE_CAN_WRITE)
+ *
+ * constraint: RLIMIT_FSIZE
+ *   desc: The size of data written is constrained by the RLIMIT_FSIZE resource
+ *     limit. In generic_write_check_limits(), a write that starts at or beyond
+ *     the limit sends SIGXFSZ and returns EFBIG. A write that starts below the
+ *     limit but would cross it is shortened to limit - pos bytes, with no
+ *     signal.
+ *   expr: pos < rlimit(RLIMIT_FSIZE) || rlimit(RLIMIT_FSIZE) == RLIM_INFINITY
+ *
+ * constraint: File seals
+ *   desc: For memfd or shmem files with F_SEAL_WRITE or F_SEAL_FUTURE_WRITE
+ *     seals applied, all write operations fail with EPERM. With F_SEAL_GROW,
+ *     writes that would extend file size fail with EPERM.
+ *
+ * examples: n = write(fd, buf, sizeof(buf));  // Basic write
+ *   n = write(STDOUT_FILENO, msg, strlen(msg));  // Write to stdout
+ *   // Handle short writes:
+ *   while (total < len) {
+ *     n = write(fd, buf + total, len - total);
+ *     if (n < 0) break;
+ *     total += n;
+ *   }
+ *   // Pipe error handling:
+ *   if (write(pipefd[1], &byte, 1) < 0 && errno == EPIPE)
+ *     handle_broken_pipe();
+ *
+ * notes: The behavior of write() varies significantly depending on the type of
+ *   file descriptor:
+ *
+ *   - Regular files: Writes to the page cache (buffered) or directly to storage
+ *     (O_DIRECT). Short writes are rare except near RLIMIT_FSIZE or disk full.
+ *     O_APPEND is atomic for determining write position.
+ *
+ *   - Pipes and FIFOs: Blocking by default. Writes up to PIPE_BUF (4096 bytes
+ *     on Linux) are guaranteed atomic. Larger writes may be interleaved with
+ *     writes from other processes. Blocks if pipe is full; returns EAGAIN with
+ *     O_NONBLOCK. SIGPIPE/EPIPE if no readers.
+ *
+ *   - Sockets: Behavior depends on socket type and protocol. Stream sockets
+ *     (TCP) may return partial writes. Datagram sockets (UDP) typically write
+ *     complete messages or fail. SIGPIPE/EPIPE for broken connections (unless
+ *     MSG_NOSIGNAL). EDESTADDRREQ for unconnected datagram sockets.
+ *
+ *   - Terminals: May block on flow control. Canonical vs raw mode affects
+ *     behavior. Special characters may be interpreted.
+ *
+ *   - Device special files: Behavior is device-specific. Block devices behave
+ *     similarly to regular files. Character device behavior varies.
+ *
+ *   Race condition considerations: Concurrent writes from threads sharing a
+ *   file description race on the file position. Linux 3.14+ provides atomic
+ *   position updates via f_pos_lock for regular files (FMODE_ATOMIC_POS), but
+ *   for maximum safety, use pwrite() for concurrent positioned writes.
+ *
+ *   O_DIRECT writes bypass the page cache and typically require buffer and
+ *   offset alignment to filesystem block size. Query requirements via statx()
+ *   with STATX_DIOALIGN (Linux 6.1+). Unaligned O_DIRECT writes return EINVAL
+ *   on most filesystems.
+ *
+ *   For zero-copy writes, consider using splice(2), sendfile(2), or vmsplice(2)
+ *   instead of copying data through user-space buffers with write().
+ *
+ *   Partial writes (short writes) must be handled by application code.
+ *   Applications should loop until all data is written or an error occurs.
+ */
 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
 		size_t, count)
 {
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/include/trace/events/kapi.h b/include/trace/events/kapi.h
new file mode 100644
index 0000000000000..47828f3338828
--- /dev/null
+++ b/include/trace/events/kapi.h
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM kapi
+
+#if !defined(_TRACE_KAPI_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_KAPI_H
+
+#include <linux/tracepoint.h>
+
+/* Max length of the rendered "name=value, ..." parameter list. */
+#define KAPI_TP_PARAMS_LEN 256
+
+/*
+ * Emitted from the CONFIG_KAPI_RUNTIME_CHECKS syscall validation path for
+ * syscalls that have a KAPI specification: kapi_syscall_enter fires before
+ * parameter validation, kapi_syscall_exit after the handler returns.
+ * @name is the spec name, e.g. "sys_open".
+ *
+ * kapi_syscall_enter carries both the raw argument values (args[]) and, when
+ * the spec provides parameter metadata, a rendered "name=value" list (params,
+ * built by the caller): pointer-like values in hex, integers and fds in decimal.
+ */
+TRACE_EVENT(kapi_syscall_enter,
+
+	TP_PROTO(const char *name, int nargs, const s64 *args, const char *params),
+
+	TP_ARGS(name, nargs, args, params),
+
+	TP_STRUCT__entry(
+		__string(	name,	name	)
+		__field(	int,	nargs	)
+		__array(	u64,	args,	6	)
+		__string(	params,	params	)
+	),
+
+	TP_fast_assign(
+		__assign_str(name);
+		__entry->nargs = nargs;
+		memset(__entry->args, 0, sizeof(__entry->args));
+		if (args && nargs > 0)
+			memcpy(__entry->args, args,
+			       min_t(int, nargs, 6) * sizeof(__entry->args[0]));
+		__assign_str(params);
+	),
+
+	TP_printk("%s(%s)", __get_str(name), __get_str(params))
+);
+
+TRACE_EVENT(kapi_syscall_exit,
+
+	TP_PROTO(const char *name, long ret, bool spec_match),
+
+	TP_ARGS(name, ret, spec_match),
+
+	TP_STRUCT__entry(
+		__string(	name,		name		)
+		__field(	long,		ret		)
+		__field(	bool,		spec_match	)
+	),
+
+	TP_fast_assign(
+		__assign_str(name);
+		__entry->ret = ret;
+		__entry->spec_match = spec_match;
+	),
+
+	TP_printk("%s = %ld spec_match=%d",
+		  __get_str(name), __entry->ret, __entry->spec_match)
+);
+
+#endif /* _TRACE_KAPI_H */
+
+/* This part must be outside protection */
+#include <trace/define_trace.h>
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..6075ab82bd231
--- /dev/null
+++ b/kernel/api/Kconfig
@@ -0,0 +1,74 @@
+# 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_SPEC_DEBUGFS
+	bool "Export kernel API specifications via debugfs"
+	depends on KAPI_SPEC
+	depends on DEBUG_FS
+	help
+	  This option enables exporting kernel API specifications through
+	  the debugfs filesystem. When enabled, specifications can be
+	  accessed at /sys/kernel/debug/kapi/.
+
+	  The debugfs interface provides:
+	  - A list of all available API specifications
+	  - Detailed information for each API including parameters,
+	    return values, errors, locking requirements, and constraints
+	  - Complete machine-readable representation of the specs
+
+	  This is useful for documentation tools, static analyzers, and
+	  runtime introspection of kernel APIs.
+
+	  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..2e1c3555d5aa5
--- /dev/null
+++ b/kernel/api/Makefile
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Makefile for the Kernel API Specification Framework
+#
+
+# Core API specification framework
+obj-y				+= kernel_api_spec.o
+
+# Debugfs interface for kernel API specs
+obj-$(CONFIG_KAPI_SPEC_DEBUGFS) += kapi_debugfs.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_debugfs.c b/kernel/api/kapi_debugfs.c
new file mode 100644
index 0000000000000..5d9c2649a0dc6
--- /dev/null
+++ b/kernel/api/kapi_debugfs.c
@@ -0,0 +1,553 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Kernel API specification debugfs interface
+ *
+ * This provides a debugfs interface to expose kernel API specifications
+ * at runtime, allowing tools and users to query the complete API specs.
+ */
+
+#include <linux/debugfs.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/seq_file.h>
+#include <linux/kernel_api_spec.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "internal.h"
+
+/* Helper to print parameter flags */
+static void print_param_flags(struct seq_file *m, u32 flags)
+{
+	seq_puts(m, "    flags: ");
+	if ((flags & KAPI_PARAM_INOUT) == KAPI_PARAM_INOUT)
+		seq_puts(m, "INOUT ");
+	else if (flags & KAPI_PARAM_IN)
+		seq_puts(m, "IN ");
+	else if (flags & KAPI_PARAM_OUT)
+		seq_puts(m, "OUT ");
+	if (flags & KAPI_PARAM_OPTIONAL)
+		seq_puts(m, "OPTIONAL ");
+	if (flags & KAPI_PARAM_CONST)
+		seq_puts(m, "CONST ");
+	if (flags & KAPI_PARAM_USER)
+		seq_puts(m, "USER ");
+	if (flags & KAPI_PARAM_VOLATILE)
+		seq_puts(m, "VOLATILE ");
+	if (flags & KAPI_PARAM_DMA)
+		seq_puts(m, "DMA ");
+	if (flags & KAPI_PARAM_ALIGNED)
+		seq_puts(m, "ALIGNED ");
+	seq_puts(m, "\n");
+}
+
+/* Helper to print context flags */
+static void print_context_flags(struct seq_file *m, u32 flags)
+{
+	seq_puts(m, "Context flags: ");
+	if (flags & KAPI_CTX_PROCESS)
+		seq_puts(m, "PROCESS ");
+	if (flags & KAPI_CTX_HARDIRQ)
+		seq_puts(m, "HARDIRQ ");
+	if (flags & KAPI_CTX_SOFTIRQ)
+		seq_puts(m, "SOFTIRQ ");
+	if (flags & KAPI_CTX_NMI)
+		seq_puts(m, "NMI ");
+	if (flags & KAPI_CTX_SLEEPABLE)
+		seq_puts(m, "SLEEPABLE ");
+	if (flags & KAPI_CTX_ATOMIC)
+		seq_puts(m, "ATOMIC ");
+	if (flags & KAPI_CTX_PREEMPT_DISABLED)
+		seq_puts(m, "PREEMPT_DISABLED ");
+	if (flags & KAPI_CTX_IRQ_DISABLED)
+		seq_puts(m, "IRQ_DISABLED ");
+	seq_puts(m, "\n");
+}
+
+/*
+ * Print a multi-line value with every line indented, so that no line of it
+ * can be mistaken for a field of its own.
+ */
+static void print_block(struct seq_file *m, const char *text)
+{
+	while (*text) {
+		size_t len = strcspn(text, "\n");
+
+		if (len)
+			seq_printf(m, "  %.*s", (int)len, text);
+		seq_putc(m, '\n');
+		text += len;
+		if (*text == '\n')
+			text++;
+	}
+}
+
+/* Helper to print a list of signed values */
+static void print_s64_list(struct seq_file *m, const char *label,
+			   const s64 *vals, u32 count)
+{
+	u32 i;
+
+	if (!vals || !count)
+		return;
+
+	seq_printf(m, "%s:", label);
+	for (i = 0; i < count; i++)
+		seq_printf(m, " %lld", vals[i]);
+	seq_puts(m, "\n");
+}
+
+/* Helper to print which parameter carries a buffer's element count */
+static void print_size_param(struct seq_file *m,
+			     const struct kernel_api_spec *spec,
+			     const struct kapi_param_spec *param)
+{
+	int idx = param->size_param_idx - 1;
+
+	if (idx < 0)
+		return;
+
+	seq_printf(m, "      size_param: %s (index %d)\n",
+		   idx < spec->param_count && idx < KAPI_MAX_PARAMS ?
+		   spec->params[idx].name : "?", idx);
+	if (param->size_multiplier)
+		seq_printf(m, "      size_multiplier: %zu\n",
+			   param->size_multiplier);
+}
+
+/* Show function for individual API spec */
+static int kapi_spec_show(struct seq_file *m, void *v)
+{
+	struct kernel_api_spec *spec = m->private;
+	int i;
+
+	seq_puts(m, "Kernel API Specification\n");
+	seq_puts(m, "========================\n\n");
+
+	/* Basic info */
+	seq_printf(m, "Name: %s\n", spec->name);
+	seq_printf(m, "Version: %u\n", spec->version);
+	seq_printf(m, "Description: %s\n", spec->description);
+	if (spec->long_description && *spec->long_description) {
+		seq_puts(m, "Long description:\n");
+		print_block(m, spec->long_description);
+	}
+
+	/* Context */
+	print_context_flags(m, spec->context_flags);
+	seq_puts(m, "\n");
+
+	/* Parameters */
+	if (spec->param_count > 0) {
+		seq_printf(m, "Parameters (%u):\n", spec->param_count);
+		for (i = 0; i < spec->param_count && i < KAPI_MAX_PARAMS; i++) {
+			struct kapi_param_spec *param = &spec->params[i];
+
+			seq_printf(m, "  [%d] %s:\n", i, param->name);
+			seq_printf(m, "    type: %s (%s)\n",
+				   kapi_param_type_to_string(param->type), param->type_name);
+			print_param_flags(m, param->flags);
+			if (param->description && *param->description)
+				seq_printf(m, "    description: %s\n", param->description);
+			if (param->size > 0)
+				seq_printf(m, "    size: %zu\n", param->size);
+			if (param->alignment > 0)
+				seq_printf(m, "    alignment: %zu\n", param->alignment);
+
+			/* Print constraints if any */
+			if (param->constraint_type != KAPI_CONSTRAINT_NONE ||
+			    (param->constraints && *param->constraints)) {
+				seq_puts(m, "    constraints:\n");
+				switch (param->constraint_type) {
+				case KAPI_CONSTRAINT_NONE:
+					break;
+				case KAPI_CONSTRAINT_RANGE:
+					seq_puts(m, "      type: range\n");
+					seq_printf(m, "      min: %lld\n", param->min_value);
+					seq_printf(m, "      max: %lld\n", param->max_value);
+					break;
+				case KAPI_CONSTRAINT_MASK:
+					seq_puts(m, "      type: mask\n");
+					seq_printf(m, "      valid_bits: 0x%llx\n",
+						   param->valid_mask);
+					break;
+				case KAPI_CONSTRAINT_ENUM:
+					seq_puts(m, "      type: enum\n");
+					seq_printf(m, "      count: %u\n", param->enum_count);
+					print_s64_list(m, "      values",
+						       param->enum_values,
+						       param->enum_count);
+					break;
+				case KAPI_CONSTRAINT_USER_STRING:
+					seq_puts(m, "      type: user_string\n");
+					seq_printf(m, "      min_len: %lld\n", param->min_value);
+					seq_printf(m, "      max_len: %lld\n", param->max_value);
+					break;
+				case KAPI_CONSTRAINT_USER_PATH:
+					seq_puts(m, "      type: user_path\n");
+					seq_puts(m, "      max_len: PATH_MAX (4096)\n");
+					break;
+				case KAPI_CONSTRAINT_USER_PTR:
+					seq_puts(m, "      type: user_ptr\n");
+					seq_printf(m, "      size: %zu bytes\n", param->size);
+					print_size_param(m, spec, param);
+					break;
+				case KAPI_CONSTRAINT_BUFFER:
+					seq_puts(m, "      type: buffer\n");
+					print_size_param(m, spec, param);
+					break;
+				case KAPI_CONSTRAINT_ALIGNMENT:
+					seq_puts(m, "      type: alignment\n");
+					seq_printf(m, "      alignment: %zu\n", param->alignment);
+					break;
+				case KAPI_CONSTRAINT_POWER_OF_TWO:
+					seq_puts(m, "      type: power_of_two\n");
+					break;
+				case KAPI_CONSTRAINT_PAGE_ALIGNED:
+					seq_puts(m, "      type: page_aligned\n");
+					break;
+				case KAPI_CONSTRAINT_NONZERO:
+					seq_puts(m, "      type: nonzero\n");
+					break;
+				case KAPI_CONSTRAINT_CUSTOM:
+					seq_puts(m, "      type: custom\n");
+					break;
+				default:
+					seq_printf(m, "      type: unknown (%d)\n",
+						   param->constraint_type);
+					break;
+				}
+				if (param->constraints && *param->constraints)
+					seq_printf(m, "      description: %s\n",
+						   param->constraints);
+			}
+			seq_puts(m, "\n");
+		}
+	}
+
+	/* Return value */
+	seq_puts(m, "Return value:\n");
+	seq_printf(m, "  type: %s\n", spec->return_spec.type_name);
+	if (spec->return_spec.description && *spec->return_spec.description)
+		seq_printf(m, "  description: %s\n", spec->return_spec.description);
+
+	switch (spec->return_spec.check_type) {
+	case KAPI_RETURN_EXACT:
+		seq_printf(m, "  success: == %lld\n", spec->return_spec.success_value);
+		break;
+	case KAPI_RETURN_RANGE:
+		seq_printf(m, "  success: [%lld, %lld]\n",
+			   spec->return_spec.success_min,
+			   spec->return_spec.success_max);
+		break;
+	case KAPI_RETURN_FD:
+		seq_puts(m, "  success: valid file descriptor (>= 0)\n");
+		break;
+	case KAPI_RETURN_ERROR_CHECK:
+		seq_puts(m, "  success: error check\n");
+		print_s64_list(m, "  error values",
+			       spec->return_spec.error_values,
+			       spec->return_spec.error_count);
+		break;
+	case KAPI_RETURN_CUSTOM:
+		seq_puts(m, "  success: custom check\n");
+		break;
+	case KAPI_RETURN_NO_RETURN:
+		seq_puts(m, "  success: does not return\n");
+		break;
+	default:
+		break;
+	}
+	seq_puts(m, "\n");
+
+	/* Errors */
+	if (spec->error_count > 0) {
+		seq_printf(m, "Errors (%u):\n", spec->error_count);
+		for (i = 0; i < spec->error_count && i < KAPI_MAX_ERRORS; i++) {
+			struct kapi_error_spec *err = &spec->errors[i];
+
+			seq_printf(m, "  %s (%d): %s\n",
+				   err->name, err->error_code, err->description);
+			if (err->condition && *err->condition)
+				seq_printf(m, "    condition: %s\n", err->condition);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Locks */
+	if (spec->lock_count > 0) {
+		seq_printf(m, "Locks (%u):\n", spec->lock_count);
+		for (i = 0; i < spec->lock_count && i < KAPI_MAX_LOCKS; i++) {
+			struct kapi_lock_spec *lock = &spec->locks[i];
+
+			seq_printf(m, "  %s (%s): %s\n", lock->lock_name,
+				   kapi_lock_type_to_string(lock->lock_type),
+				   lock->description);
+			seq_printf(m, "    scope: %s\n",
+				   kapi_lock_scope_to_string(lock->scope));
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Constraints */
+	if (spec->constraint_count > 0) {
+		seq_printf(m, "Additional constraints (%u):\n", spec->constraint_count);
+		for (i = 0; i < spec->constraint_count && i < KAPI_MAX_CONSTRAINTS; i++) {
+			struct kapi_constraint_spec *cons = &spec->constraints[i];
+
+			seq_printf(m, "  - %s", cons->name);
+			if (cons->description && *cons->description)
+				seq_printf(m, ": %s", cons->description);
+			seq_puts(m, "\n");
+			if (cons->expression && *cons->expression)
+				seq_printf(m, "    expression: %s\n", cons->expression);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Signals */
+	if (spec->signal_count > 0) {
+		seq_printf(m, "Signal handling (%u):\n", spec->signal_count);
+		for (i = 0; i < spec->signal_count && i < KAPI_MAX_SIGNALS; i++) {
+			struct kapi_signal_spec *sig = &spec->signals[i];
+
+			seq_printf(m, "  %s (%d):\n", sig->signal_name, sig->signal_num);
+			seq_puts(m, "    direction: ");
+			if (sig->direction & KAPI_SIGNAL_SEND)
+				seq_puts(m, "send ");
+			if (sig->direction & KAPI_SIGNAL_RECEIVE)
+				seq_puts(m, "receive ");
+			if (sig->direction & KAPI_SIGNAL_HANDLE)
+				seq_puts(m, "handle ");
+			if (sig->direction & KAPI_SIGNAL_BLOCK)
+				seq_puts(m, "block ");
+			if (sig->direction & KAPI_SIGNAL_IGNORE)
+				seq_puts(m, "ignore ");
+			seq_puts(m, "\n");
+			seq_puts(m, "    action: ");
+			switch (sig->action) {
+			case KAPI_SIGNAL_ACTION_DEFAULT:
+				seq_puts(m, "default");
+				break;
+			case KAPI_SIGNAL_ACTION_TERMINATE:
+				seq_puts(m, "terminate");
+				break;
+			case KAPI_SIGNAL_ACTION_COREDUMP:
+				seq_puts(m, "coredump");
+				break;
+			case KAPI_SIGNAL_ACTION_STOP:
+				seq_puts(m, "stop");
+				break;
+			case KAPI_SIGNAL_ACTION_CONTINUE:
+				seq_puts(m, "continue");
+				break;
+			case KAPI_SIGNAL_ACTION_CUSTOM:
+				seq_puts(m, "custom");
+				break;
+			case KAPI_SIGNAL_ACTION_RETURN:
+				seq_puts(m, "return");
+				break;
+			case KAPI_SIGNAL_ACTION_RESTART:
+				seq_puts(m, "restart");
+				break;
+			case KAPI_SIGNAL_ACTION_QUEUE:
+				seq_puts(m, "queue");
+				break;
+			case KAPI_SIGNAL_ACTION_DISCARD:
+				seq_puts(m, "discard");
+				break;
+			case KAPI_SIGNAL_ACTION_TRANSFORM:
+				seq_puts(m, "transform");
+				break;
+			default:
+				seq_puts(m, "unknown");
+				break;
+			}
+			seq_puts(m, "\n");
+			if (sig->description && *sig->description)
+				seq_printf(m, "    description: %s\n", sig->description);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Side effects */
+	if (spec->side_effect_count > 0) {
+		seq_printf(m, "Side effects (%u):\n", spec->side_effect_count);
+		for (i = 0; i < spec->side_effect_count && i < KAPI_MAX_SIDE_EFFECTS; i++) {
+			const struct kapi_side_effect *eff = &spec->side_effects[i];
+
+			seq_printf(m, "  - %s", eff->target);
+			if (eff->description && *eff->description)
+				seq_printf(m, ": %s", eff->description);
+			if (eff->reversible)
+				seq_puts(m, " (reversible)");
+			seq_puts(m, "\n");
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* State transitions */
+	if (spec->state_trans_count > 0) {
+		seq_printf(m, "State transitions (%u):\n", spec->state_trans_count);
+		for (i = 0; i < spec->state_trans_count && i < KAPI_MAX_STATE_TRANS; i++) {
+			const struct kapi_state_transition *trans = &spec->state_transitions[i];
+
+			seq_printf(m, "  %s: %s -> %s\n", trans->object,
+				   trans->from_state, trans->to_state);
+			if (trans->description && *trans->description)
+				seq_printf(m, "    %s\n", trans->description);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Capabilities */
+	if (spec->capability_count > 0) {
+		seq_printf(m, "Capabilities (%u):\n", spec->capability_count);
+		for (i = 0; i < spec->capability_count && i < KAPI_MAX_CAPABILITIES; i++) {
+			const struct kapi_capability_spec *cap = &spec->capabilities[i];
+
+			seq_printf(m, "  %s (%d):\n", cap->cap_name,
+				   cap->capability);
+			if (cap->allows && *cap->allows)
+				seq_printf(m, "    allows: %s\n", cap->allows);
+			if (cap->without_cap && *cap->without_cap)
+				seq_printf(m, "    without: %s\n", cap->without_cap);
+		}
+		seq_puts(m, "\n");
+	}
+
+	/* Additional info */
+	if (spec->examples && *spec->examples) {
+		seq_puts(m, "Examples:\n");
+		print_block(m, spec->examples);
+		seq_putc(m, '\n');
+	}
+	if (spec->notes && *spec->notes) {
+		seq_puts(m, "Notes:\n");
+		print_block(m, spec->notes);
+		seq_putc(m, '\n');
+	}
+
+	return 0;
+}
+
+static int kapi_spec_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, kapi_spec_show, inode->i_private);
+}
+
+static const struct file_operations kapi_spec_fops = {
+	.open = kapi_spec_open,
+	.read = seq_read,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+/*
+ * JSON view of a single spec.  kapi_export_json() writes straight into the
+ * seq_file buffer; when the output does not fit, flagging an overflow makes
+ * seq_file retry with a larger buffer, so no fixed size limit applies.
+ */
+static int kapi_spec_json_show(struct seq_file *m, void *v)
+{
+	const struct kernel_api_spec *spec = m->private;
+	size_t size;
+	char *buf;
+	int ret;
+
+	if (!spec)
+		return -EINVAL;
+
+	size = seq_get_buf(m, &buf);
+	ret = size ? kapi_export_json(spec, buf, size) : -E2BIG;
+	if (ret == -E2BIG) {
+		seq_commit(m, -1);
+		return 0;
+	}
+	if (ret < 0)
+		return ret;
+
+	seq_commit(m, ret);
+	return 0;
+}
+
+static int kapi_spec_json_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, kapi_spec_json_show, inode->i_private);
+}
+
+static const struct file_operations kapi_spec_json_fops = {
+	.open = kapi_spec_json_open,
+	.read = seq_read,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+/*
+ * Show all available API specs.
+ *
+ * Note: This only iterates the static .kapi_specs section. Specs registered
+ * dynamically via kapi_register_spec() are not included in this listing
+ * or in the per-spec debugfs files.
+ */
+static int kapi_list_show(struct seq_file *m, void *v)
+{
+	const struct kernel_api_spec * const *pp;
+	int count = 0;
+
+	seq_puts(m, "Available Kernel API Specifications\n");
+	seq_puts(m, "===================================\n\n");
+
+	for (pp = __start_kapi_specs; pp < __stop_kapi_specs; pp++) {
+		const struct kernel_api_spec *spec = *pp;
+
+		if (!spec)
+			continue;
+		seq_printf(m, "%s - %s\n", spec->name, spec->description);
+		count++;
+	}
+
+	seq_printf(m, "\nTotal: %d specifications\n", count);
+	return 0;
+}
+
+static int kapi_list_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, kapi_list_show, NULL);
+}
+
+static const struct file_operations kapi_list_fops = {
+	.open = kapi_list_open,
+	.read = seq_read,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int __init kapi_debugfs_init(void)
+{
+	const struct kernel_api_spec * const *pp;
+	struct dentry *root, *spec_dir, *json_dir;
+
+	root = debugfs_create_dir("kapi", NULL);
+	debugfs_create_file("list", 0444, root, NULL, &kapi_list_fops);
+	spec_dir = debugfs_create_dir("specs", root);
+	json_dir = debugfs_create_dir("specs-json", root);
+
+	for (pp = __start_kapi_specs; pp < __stop_kapi_specs; pp++) {
+		const struct kernel_api_spec *spec = *pp;
+
+		if (!spec || !spec->name)
+			continue;
+		debugfs_create_file(spec->name, 0444, spec_dir,
+				    (void *)spec, &kapi_spec_fops);
+		debugfs_create_file(spec->name, 0444, json_dir,
+				    (void *)spec, &kapi_spec_json_fops);
+	}
+
+	return 0;
+}
+
+/* Initialize as part of kernel, not as a module */
+fs_initcall(kapi_debugfs_init);
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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 50));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 100));
+
+	param.min_value = -10;
+	param.max_value = -1;
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, -10));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, -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(&param, 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(&param, -1));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 101));
+
+	param.min_value = -10;
+	param.max_value = -1;
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, -11));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 0));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 0x00));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 0x0F));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 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(&param, 0x100));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 1));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 2));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 4));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 8));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 0));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 3));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, PAGE_SIZE));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 2 * PAGE_SIZE));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 1));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 1));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, -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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 8));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 16));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 1));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 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(&param, 1));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 5));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 10));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 0));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 3));
+	KUNIT_EXPECT_FALSE(test, kapi_validate_param(&param, 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(&param, 0));
+	KUNIT_EXPECT_TRUE(test, kapi_validate_param(&param, 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(&param);
+
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, NULL, 0));
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, (void __user *)PAGE_SIZE, 0));
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, 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(&param);
+
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, NULL, 1));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, 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(&param);
+
+	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(&param, kernel_addr, 1));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, 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(&param);
+
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, (void __user *)PAGE_SIZE, 16));
+	KUNIT_EXPECT_TRUE(test, validate_dyn_buf(&param, 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(&param, near_top, PAGE_SIZE));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, 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(&param);
+	param.size_multiplier = 8;
+
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, (void __user *)PAGE_SIZE,
+						  (s64)(SIZE_MAX / 8 + 1)));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, (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(&param, near_top, PAGE_SIZE / 8));
+	KUNIT_EXPECT_FALSE(test, validate_dyn_buf(&param, 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..993674ce67789
--- /dev/null
+++ b/kernel/api/kernel_api_spec.c
@@ -0,0 +1,1470 @@
+// 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
+
+#define CREATE_TRACE_POINTS
+#include <trace/events/kapi.h>
+
+/*
+ * Render a syscall's parameters as a "name=value, ..." string for the
+ * kapi_syscall_enter tracepoint.  Names come from the spec; pointer-like
+ * values are shown in hex, integers and file descriptors in decimal.
+ */
+static void kapi_trace_format_params(const struct kernel_api_spec *spec,
+				     const s64 *args, int nargs,
+				     char *buf, size_t size)
+{
+	int i, used = 0;
+
+	buf[0] = '\0';
+	/* Bound by the caller-supplied arg count; the spec arity may differ. */
+	for (i = 0; args && i < nargs && i < 6; i++) {
+		const char *name = "arg";
+		bool dec = false;
+
+		if (i < spec->param_count) {
+			const struct kapi_param_spec *ps = &spec->params[i];
+
+			if (ps->name)
+				name = ps->name;
+			dec = ps->type == KAPI_TYPE_INT || ps->type == KAPI_TYPE_FD;
+		}
+
+		used += scnprintf(buf + used, size - used, "%s%s=",
+				  i ? ", " : "", name);
+		if (dec)
+			used += scnprintf(buf + used, size - used, "%lld",
+					  (long long)args[i]);
+		else
+			used += scnprintf(buf + used, size - used, "0x%llx",
+					  (unsigned long long)args[i]);
+	}
+}
+
+/**
+ * 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, ret = 0;
+
+	if (!spec || !params)
+		return 0;
+
+	if (trace_kapi_syscall_enter_enabled()) {
+		char pbuf[KAPI_TP_PARAMS_LEN];
+
+		kapi_trace_format_params(spec, params, param_count, pbuf, sizeof(pbuf));
+		trace_kapi_syscall_enter(spec->name, param_count, params, pbuf);
+	}
+
+	/* 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);
+		ret = -EINVAL;
+		goto out;
+	}
+
+	/* 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 */
+				ret = -EINVAL;
+				goto out;
+			}
+		}
+	}
+
+out:
+	/*
+	 * Emit the exit event on the rejection path too (the wrapper
+	 * short-circuits the handler on a non-zero return), so every
+	 * kapi_syscall_enter has a matching kapi_syscall_exit.
+	 */
+	if (ret)
+		trace_kapi_syscall_exit(spec->name, ret, false);
+
+	return ret;
+}
+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)
+{
+	bool valid = true;
+
+	if (!spec)
+		return 0;
+
+	/* Validate against the return spec when one was defined */
+	if (spec->return_magic == KAPI_MAGIC_RETURN)
+		valid = kapi_validate_return_value(spec, retval);
+
+	trace_kapi_syscall_exit(spec->name, retval, valid);
+
+	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 */
diff --git a/mm/madvise.c b/mm/madvise.c
index eeee82cf2b3f4..4eb29922a2150 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -2018,6 +2018,555 @@ int do_madvise(struct mm_struct *mm, unsigned long start, size_t len_in, int beh
 	return error;
 }
 
+/**
+ * sys_madvise - Give advice about use of memory
+ * @start: Starting virtual address of the range to advise on
+ * @len_in: Length of the range in bytes
+ * @behavior: Advice (a MADV_* constant) the kernel should apply to the range
+ *
+ * long-desc: Provides the kernel with advice or directions about the address
+ *   range starting at start and extending for len_in bytes. The advice is
+ *   selected by behavior, which is one of the MADV_* constants defined in
+ *   <sys/mman.h>. The behaviors fall into three groups. The hint group
+ *   updates VMA flags (MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL,
+ *   MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP, MADV_WIPEONFORK,
+ *   MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE, MADV_HUGEPAGE,
+ *   MADV_NOHUGEPAGE). The immediate-action group performs work synchronously
+ *   while preserving page contents (MADV_WILLNEED, MADV_COLD, MADV_PAGEOUT,
+ *   MADV_POPULATE_READ, MADV_POPULATE_WRITE, MADV_COLLAPSE, MADV_GUARD_REMOVE,
+ *   MADV_SOFT_OFFLINE). The destructive group discards, replaces or
+ *   invalidates page contents (MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE,
+ *   MADV_REMOVE, MADV_GUARD_INSTALL, MADV_HWPOISON). MADV_GUARD_INSTALL
+ *   belongs to the destructive group because it zaps any existing pages in
+ *   the range before installing PTE guard markers.
+ *
+ *   start must be page-aligned; len_in is rounded up to the next page
+ *   boundary internally. Once those validation checks pass, a zero-length
+ *   range succeeds without performing work. The kernel rejects ranges that
+ *   wrap (start + PAGE_ALIGN(len_in) < start) and ranges where len_in is
+ *   non-zero but rounds up to zero. Address tagging bits are stripped
+ *   from start before VMA lookup for every behavior except MADV_HWPOISON
+ *   and MADV_SOFT_OFFLINE, which receive the raw start value because they
+ *   bypass the VMA walk entirely.
+ *
+ *   The kernel return value reports whether any error condition was
+ *   encountered, not whether the requested work was performed. The
+ *   relationship between the return code and the work done varies by
+ *   handler:
+ *
+ *     - Hint behaviors set or clear VMA flags; how the flag is used later
+ *       depends on the behavior. MADV_DONTFORK / MADV_DOFORK set or clear
+ *       VM_DONTCOPY, which dup_mmap() honors, so the child does not get the
+ *       mapping. MADV_MERGEABLE / MADV_UNMERGEABLE control whether KSM scans
+ *       the VMA. MADV_NOHUGEPAGE blocks the fault-time, MADV_COLLAPSE and
+ *       khugepaged THP paths for the VMA. MADV_HUGEPAGE makes the VMA
+ *       eligible for THP when the transparent_hugepage mode is "madvise" and
+ *       increases defrag effort; it does not force allocation, which still
+ *       depends on the global mode, VMA suitability, defrag GFP policy, and
+ *       allocation or memcg-charge success. MADV_WIPEONFORK does not wipe
+ *       pages at fork time; the child VMA's pages are not copied, and the
+ *       child sees zero-filled pages when it first touches them.
+ *       MADV_KEEPONFORK clears VM_WIPEONFORK. MADV_DONTDUMP / MADV_DODUMP set
+ *       or clear VM_DONTDUMP, although always_dump_vma() still includes gate,
+ *       vm_ops-named or arch-named VMAs in a core dump. MADV_NORMAL /
+ *       MADV_RANDOM / MADV_SEQUENTIAL set or clear VM_RAND_READ / VM_SEQ_READ,
+ *       which only steer read-ahead.
+ *
+ *     - Walk-and-skip handlers (MADV_COLD, MADV_PAGEOUT, MADV_FREE,
+ *       MADV_GUARD_REMOVE) traverse the range and silently skip pages or
+ *       PMDs that fail per-page preconditions (absent, special, device,
+ *       shared, non-LRU, unsplittable, locked, etc.), returning 0 even
+ *       when most or all pages were skipped.
+ *
+ *     - Bulk-backend handlers delegate the requested range to a single
+ *       backend call: MADV_DONTNEED and MADV_DONTNEED_LOCKED to
+ *       zap_vma_range_batched(), MADV_REMOVE to vfs_fallocate(),
+ *       MADV_WILLNEED on regular files to vfs_fadvise(). The backend's
+ *       return is propagated for MADV_REMOVE and discarded for
+ *       MADV_WILLNEED; DAX files short-circuit MADV_WILLNEED entirely.
+ *
+ *     - Stop-on-error handlers (MADV_POPULATE_READ, MADV_POPULATE_WRITE,
+ *       MADV_SOFT_OFFLINE) walk the range but surface the first per-page
+ *       failure as an errno (-EHWPOISON, -EFAULT, -ENOMEM, ...) rather
+ *       than skipping silently.
+ *
+ *     - Hybrid handlers combine modes: MADV_WILLNEED walks for anonymous
+ *       and shmem ranges but bulk-calls vfs_fadvise() for regular files;
+ *       MADV_COLLAPSE walks PMD-by-PMD and tracks the last scan failure
+ *       so transient skips coexist with terminal errors;
+ *       MADV_GUARD_INSTALL walks to install markers and re-walks after
+ *       zap_vma_range() to clear pre-existing pages, retrying up to
+ *       MAX_MADVISE_GUARD_RETRIES; MADV_HWPOISON walks pages but folds
+ *       memory_failure()'s -EOPNOTSUPP back to 0.
+ *
+ *   Applications that need to know whether a specific page was acted on
+ *   must verify the result through other means (e.g. /proc/[pid]/smaps,
+ *   page faults, read-after-write).
+ *
+ *   On success, madvise() returns 0; unlike read(2) and write(2) it has no
+ *   notion of partial completion at the syscall boundary. When the range
+ *   spans multiple VMAs, the kernel applies the advice to each in turn. For
+ *   the behaviors that walk VMAs (all except MADV_POPULATE_*, MADV_HWPOISON
+ *   and MADV_SOFT_OFFLINE), an unmapped gap inside the range causes the call
+ *   to return -ENOMEM after processing the mapped portions, rather than
+ *   aborting at the gap, although the walk stops at the first per-VMA error.
+ *   MADV_POPULATE_* stops at the first failure, and MADV_HWPOISON and
+ *   MADV_SOFT_OFFLINE bypass the walk, so an unmapped address gives -EFAULT.
+ *
+ *   POSIX defines posix_madvise(3) for a portable subset (POSIX_MADV_NORMAL,
+ *   _RANDOM, _SEQUENTIAL, _WILLNEED, _DONTNEED). Linux MADV_DONTNEED is
+ *   destructive: it discards the contents of the affected anonymous pages and
+ *   subsequent reads return zero. POSIX permits but does not require
+ *   destruction, so portable code that needs the POSIX semantics should use
+ *   posix_madvise(3) instead.
+ *
+ * contexts: process, sleepable
+ *
+ * param: start
+ *   type: uint, input
+ *   constraint-type: page_aligned
+ *   cdesc: Starting virtual address of the range. Must be aligned to
+ *     PAGE_SIZE. An unaligned start always returns -EINVAL, even when
+ *     len_in is zero. Address tag bits, where supported by the architecture,
+ *     are cleared via untagged_addr() before the range is interpreted, with
+ *     the exception of MADV_HWPOISON and MADV_SOFT_OFFLINE, which receive
+ *     the raw start value because they bypass the VMA walk.
+ *
+ * param: len_in
+ *   type: uint, input
+ *   cdesc: Length of the range in bytes. Internally rounded up to a multiple
+ *     of PAGE_SIZE. A len_in of 0 is accepted and the call is a no-op that
+ *     returns 0. A non-zero len_in that rounds up to 0 (i.e. wraps around)
+ *     returns -EINVAL, as does a range whose end (start + PAGE_ALIGN(len_in))
+ *     would wrap below start.
+ *
+ * param: behavior
+ *   type: int, input
+ *   cdesc: One of the MADV_* constants from <sys/mman.h>. See the long
+ *     description above for the full list and the three semantic groups
+ *     (hint, immediate-action, destructive). Behaviors gated by Kconfig
+ *     (KSM, transparent hugepage, memory failure) return -EINVAL when the
+ *     underlying support is disabled. A few architectures (notably alpha)
+ *     renumber values; portable code should always use the symbolic names.
+ *
+ * return:
+ *   type: int
+ *   check-type: exact
+ *   success: 0
+ *   desc: On success, returns 0. On error, returns a negative error code.
+ *     There is no partial-success indication; either the entire processed
+ *     range succeeded, or an error is returned and an unspecified prefix of
+ *     the range may have been advised.
+ *
+ * error: EINVAL, Invalid argument
+ *   desc: Invalid input (unknown or Kconfig-disabled MADV_*, unaligned start,
+ *     range wrap, non-zero len_in rounding up to zero) or a VMA filter.
+ *     DONTNEED/FREE reject VM_PFNMAP, VM_LOCKED (not DONTNEED_LOCKED) and
+ *     misaligned hugetlb; FREE non-anonymous; WIPEONFORK file or shared; REMOVE
+ *     VM_LOCKED or no file; COLD/PAGEOUT LOCKED/PFNMAP/HUGETLB; DOFORK
+ *     VM_SPECIAL; KEEPONFORK VM_DROPPABLE; DODUMP SPECIAL/DROPPABLE; GUARD_*
+ *     SPECIAL/HUGETLB (INSTALL also LOCKED); COLLAPSE if not possible;
+ *     POPULATE_* on bad permissions.
+ *
+ * error: ENOMEM, Cannot allocate memory
+ *   desc: For VMA-walking behaviors, a gap between mapped VMAs inside the range
+ *     gives -ENOMEM after the mapped subranges have been processed, unless a
+ *     per-VMA error ends the walk first. MADV_POPULATE_* stops at the first
+ *     failure and returns -ENOMEM when the region has no VMA or
+ *     faultin_page_range() exhausts memory. MADV_COLLAPSE returns -ENOMEM when
+ *     its struct collapse_control cannot be allocated, and when
+ *     madvise_collapse_errno() maps SCAN_ALLOC_HUGE_PAGE_FAIL (no hugepage
+ *     available) to it.
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ *   desc: For the VMA-flag-mutating behaviors, an internal -ENOMEM from VMA
+ *     splitting is translated to -EAGAIN before being returned to userspace,
+ *     advising the caller that a transient kernel resource shortage
+ *     prevented the update. Also returned by MADV_COLLAPSE via
+ *     madvise_collapse_errno() for transient scan failures (folio lock
+ *     contention, LRU isolation failure, dirty/writeback) where retrying
+ *     the call may succeed.
+ *
+ * error: EIO, Input/output error
+ *   desc: For MADV_REMOVE, an I/O error from the underlying filesystem's
+ *     FALLOC_FL_PUNCH_HOLE handler is propagated back as -EIO, and
+ *     MADV_SOFT_OFFLINE returns -EIO when soft_offline_page() cannot handle
+ *     the page. MADV_HWPOISON reports an unhandled page as -EBUSY; -EIO can
+ *     reach it only from a ZONE_DEVICE pagemap's ->memory_failure()
+ *     callback. MADV_WILLNEED and MADV_PAGEOUT do not surface filesystem or
+ *     device I/O errors: vfs_fadvise() returns are discarded by
+ *     madvise_willneed() and the pageout walk is invoked through a void
+ *     helper, so transient I/O failures during read-ahead or page-out are
+ *     silently dropped.
+ *
+ * error: EBADF, Bad file descriptor
+ *   desc: Returned by MADV_WILLNEED when applied to a non-file-backed VMA
+ *     and the kernel was built without CONFIG_SWAP, so there is neither a
+ *     file to read-ahead from nor a swap device to fault from.
+ *
+ * error: EACCES, Permission denied
+ *   desc: Returned by MADV_REMOVE when the target VMA fails
+ *     vma_is_shared_maywrite(), which needs both VM_SHARED and VM_MAYWRITE.
+ *     Private file mappings and shared mappings of files not opened for
+ *     writing are refused, while a PROT_READ MAP_SHARED mapping of a file
+ *     opened O_RDWR is accepted. Punching a hole through a refused mapping
+ *     would either be invisible to other mappers or bypass file write
+ *     permission.
+ *
+ * error: EPERM, Operation not permitted
+ *   desc: Returned in two situations. First, MADV_HWPOISON and
+ *     MADV_SOFT_OFFLINE require CAP_SYS_ADMIN; the inject-error handler
+ *     refuses non-privileged callers. Second, on 64-bit kernels, a discard
+ *     operation (MADV_FREE, MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_REMOVE,
+ *     MADV_DONTFORK, MADV_WIPEONFORK, MADV_GUARD_INSTALL) is refused on a
+ *     read-only anonymous VMA that has been sealed with mseal(2), to prevent
+ *     bypassing the seal by discarding mapped data.
+ *
+ * error: EINTR, Interrupted system call
+ *   desc: Returned when a fatal signal is delivered while the call is
+ *     waiting to acquire the mmap write lock for a VMA-flag-mutating
+ *     behavior (mmap_write_lock_killable() returns -EINTR), or when
+ *     MADV_POPULATE_READ/MADV_POPULATE_WRITE is interrupted while faulting
+ *     in pages (faultin_page_range() returns -EINTR). Only a fatal signal
+ *     interrupts these waits, so the task is being killed and user space
+ *     does not normally see the error.
+ *
+ * error: EHWPOISON, Memory page has hardware error
+ *   desc: MADV_POPULATE_READ or MADV_POPULATE_WRITE encountered a page that
+ *     has been marked as containing a hardware-detected memory error and
+ *     could not be faulted in. MADV_HWPOISON also returns it when
+ *     memory_failure() finds the page already poisoned.
+ *
+ * error: EFAULT, Bad address
+ *   desc: MADV_POPULATE_READ or MADV_POPULATE_WRITE attempted to fault in a
+ *     page whose mapping raised VM_FAULT_SIGBUS or VM_FAULT_SIGSEGV (for
+ *     example, a file-backed page beyond the end of the file).
+ *     MADV_HWPOISON and MADV_SOFT_OFFLINE return a get_user_pages_fast()
+ *     failure, typically -EFAULT for an unmapped address.
+ *
+ * error: EBUSY, Device or resource busy
+ *   desc: Returned by MADV_COLLAPSE via madvise_collapse_errno() in two
+ *     specific scan-failure modes: SCAN_CGROUP_CHARGE_FAIL (the new
+ *     hugepage cannot be charged to the memory cgroup) and
+ *     SCAN_EXCEED_NONE_PTE (too many absent PTEs in the candidate range
+ *     for a synchronous collapse). Other transient collapse failures are
+ *     reported as -EAGAIN; non-transient ones as -EINVAL. MADV_HWPOISON and
+ *     MADV_SOFT_OFFLINE also return -EBUSY when the page cannot be handled.
+ *
+ * error: EOPNOTSUPP, Operation not supported
+ *   desc: MADV_REMOVE propagates vfs_fallocate() errors verbatim, so a
+ *     filesystem without FALLOC_FL_PUNCH_HOLE support fails with -EOPNOTSUPP
+ *     (other propagated errors include -EPERM, -ETXTBSY and -ENOSPC).
+ *     MADV_SOFT_OFFLINE also returns it when soft offlining is disabled via
+ *     /proc/sys/vm/enable_soft_offline or the event is filtered by
+ *     hwpoison_filter(); MADV_HWPOISON folds that case to 0.
+ *
+ * lock: mm->mmap_lock (read mode)
+ *   type: semaphore
+ *   acquired: yes
+ *   released: yes
+ *   desc: Taken for read for MADV_REMOVE, MADV_WILLNEED, MADV_COLD,
+ *     MADV_PAGEOUT, MADV_COLLAPSE, MADV_POPULATE_READ and
+ *     MADV_POPULATE_WRITE, and as the fallback when the per-VMA lock path
+ *     declines. It is dropped and retaken around vfs_fadvise() (WILLNEED on
+ *     regular files), vfs_fallocate() and userfaultfd_remove() (REMOVE, and
+ *     DONTNEED or FREE with UFFD_FEATURE_EVENT_REMOVE), the anon and file
+ *     collapse paths (COLLAPSE) and inside faultin_page_range()
+ *     (POPULATE_*), so the VMA must be looked up again afterwards.
+ *
+ * lock: mm->mmap_lock (write mode; killable)
+ *   type: semaphore
+ *   acquired: yes
+ *   released: yes
+ *   desc: Acquired in killable write mode for behaviors that modify
+ *     vma->vm_flags or split/merge VMAs (MADV_NORMAL, MADV_RANDOM,
+ *     MADV_SEQUENTIAL, MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP,
+ *     MADV_WIPEONFORK, MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE,
+ *     MADV_HUGEPAGE, MADV_NOHUGEPAGE). If the acquisition is killed by a
+ *     fatal signal, the syscall returns -EINTR before any VMA is touched.
+ *
+ * lock: per-VMA read lock (vma->vm_refcnt)
+ *   type: custom
+ *   acquired: yes
+ *   released: yes
+ *   desc: Tried first for MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE,
+ *     MADV_GUARD_INSTALL and MADV_GUARD_REMOVE via lock_vma_under_rcu(). The
+ *     lock is a reference on the VMA (vm_refcnt), not an rwsem. The per-VMA
+ *     path is taken only when the requested range fits within a single VMA,
+ *     the target mm is the caller's mm, the VMA is not armed with
+ *     userfaultfd, and, for MADV_GUARD_INSTALL on an anonymous VMA, an
+ *     anon_vma is already attached. Otherwise the code falls back to the
+ *     mmap read lock above.
+ *
+ * lock: mmu_gather TLB batch
+ *   type: custom
+ *   acquired: yes
+ *   released: yes
+ *   desc: For MADV_DONTNEED, MADV_DONTNEED_LOCKED and MADV_FREE a single
+ *     tlb_gather_mmu() / tlb_finish_mmu() pair (madvise_init_tlb() and
+ *     madvise_finish_tlb()) wraps the whole syscall, batching TLB invalidation
+ *     across all VMAs in the range. MADV_COLD and MADV_PAGEOUT build a
+ *     short-lived gather inside the handler. MADV_GUARD_INSTALL builds a
+ *     transient gather via zap_vma_range() each time the retry loop clears
+ *     pre-existing pages, and none if the range is already empty.
+ *     MADV_GUARD_REMOVE never gathers.
+ *
+ * lock: mmu_notifier invalidate range
+ *   type: custom
+ *   acquired: yes
+ *   released: yes
+ *   desc: All zap-based paths -- MADV_DONTNEED, MADV_DONTNEED_LOCKED, the
+ *     zap branch of MADV_GUARD_INSTALL via zap_vma_range(), and
+ *     MADV_FREE's own walk -- bracket their work with
+ *     mmu_notifier_invalidate_range_start()/_end() so secondary MMUs (KVM,
+ *     IOMMU SVA, etc.) observe the page clearing.
+ *
+ * signal: Any fatal signal
+ *   direction: receive
+ *   action: return
+ *   condition: Acquiring the mmap write lock or faulting in pages for
+ *     MADV_POPULATE_*
+ *   desc: A pending fatal signal aborts mmap_write_lock_killable() (used by
+ *     the VMA-flag-mutating behaviors) and faultin_page_range() (used by
+ *     MADV_POPULATE_READ and MADV_POPULATE_WRITE), in both cases surfacing as
+ *     -EINTR. Only a fatal signal interrupts these waits, so the task is
+ *     being killed and user space does not normally see the error.
+ *   errno: -EINTR
+ *   timing: during
+ *   restartable: no
+ *
+ * side-effect: modify_state
+ *   target: vma->vm_flags
+ *   condition: Hint-group behaviors (MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL,
+ *     MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP, MADV_WIPEONFORK,
+ *     MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE, MADV_HUGEPAGE,
+ *     MADV_NOHUGEPAGE)
+ *   desc: Sets or clears VM_RAND_READ, VM_SEQ_READ, VM_DONTCOPY,
+ *     VM_DONTDUMP, VM_WIPEONFORK, VM_MERGEABLE, VM_HUGEPAGE or VM_NOHUGEPAGE
+ *     on the affected VMAs, splitting or merging VMAs as needed. Reversible
+ *     with the inverse advice (MADV_DOFORK undoes MADV_DONTFORK), except
+ *     that the inverse call's VMA filter still applies (DOFORK rejects
+ *     VM_SPECIAL, DODUMP rejects non-hugetlb VM_SPECIAL or VM_DROPPABLE,
+ *     KEEPONFORK rejects VM_DROPPABLE).
+ *   reversible: yes
+ *
+ * side-effect: free_memory | modify_state | irreversible
+ *   target: page tables and resident pages within the range
+ *   condition: MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE
+ *   desc: MADV_DONTNEED zaps PTEs, releasing the pages or swap slots so the
+ *     next access faults in zero-filled anonymous pages or re-reads the
+ *     file. MADV_DONTNEED_LOCKED is identical but tolerates VM_LOCKED.
+ *     MADV_FREE marks anonymous pages lazy-freeable; clean pages may be
+ *     reclaimed under memory pressure and a write before reclaim cancels
+ *     the lazy free. Discarded data cannot be recovered.
+ *   reversible: no
+ *
+ * side-effect: filesystem | irreversible
+ *   target: backing file (FALLOC_FL_PUNCH_HOLE)
+ *   condition: MADV_REMOVE
+ *   desc: Calls vfs_fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE) on
+ *     the backing file, deallocating the corresponding file blocks. The hole
+ *     is visible to all mappers of the file and to read(2)/write(2)
+ *     callers; subsequent reads return zero. Filesystem freeze protection,
+ *     i_rwsem and any quota/space accounting are taken by the underlying
+ *     fallocate path.
+ *   reversible: no
+ *
+ * side-effect: modify_state | schedule
+ *   target: LRU lists and page reclaim
+ *   condition: MADV_COLD, MADV_PAGEOUT
+ *   desc: MADV_COLD deactivates the affected pages, moving them to the
+ *     inactive LRU and clearing PG_referenced/PG_young so they are reclaimed
+ *     sooner under pressure. MADV_PAGEOUT additionally calls reclaim_pages()
+ *     to write dirty pages out and drop clean ones synchronously. Page data
+ *     is preserved (rereads will fault in the same content), but the I/O and
+ *     LRU bookkeeping cannot be undone.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: page tables (faultin)
+ *   condition: MADV_POPULATE_READ, MADV_POPULATE_WRITE
+ *   desc: Walks the requested range with faultin_page_range(), populating
+ *     PTEs by triggering read or write faults so subsequent accesses do not
+ *     fault. Equivalent to touching every page in the range while suppressing
+ *     SIGBUS/SIGSEGV through the syscall return value. Allocations made by
+ *     faultin are not undone on partial failure.
+ *   reversible: no
+ *
+ * side-effect: modify_state | schedule
+ *   target: transparent hugepage layout
+ *   condition: MADV_COLLAPSE
+ *   desc: Synchronously coalesces base pages in the range into a PMD-sized
+ *     transparent hugepage when the mapping permits. Performs the same page
+ *     migration and zeroing that khugepaged would do asynchronously; the
+ *     range's data is preserved across the collapse.
+ *   reversible: no
+ *
+ * side-effect: free_memory | modify_state | irreversible
+ *   target: PTE marker (PTE_MARKER_GUARD)
+ *   condition: MADV_GUARD_INSTALL, MADV_GUARD_REMOVE
+ *   desc: MADV_GUARD_INSTALL installs PTE_MARKER_GUARD entries that cause
+ *     subsequent accesses to deliver SIGSEGV without consuming physical
+ *     memory; existing pages already mapped in the range are zapped via
+ *     zap_vma_range() before the markers are installed, so any
+ *     prior contents are lost. MADV_GUARD_REMOVE clears the markers but
+ *     does not (and cannot) restore zapped data.
+ *   reversible: no
+ *
+ * side-effect: hardware | irreversible
+ *   target: physical page (memory_failure)
+ *   condition: MADV_HWPOISON
+ *   desc: MADV_HWPOISON marks the affected pages as containing an
+ *     unrecoverable hardware error using the same machine-check path that
+ *     real ECC failures take. This affects physical memory bookkeeping
+ *     kernel-wide, and madvise() has no inverse. The poison can be cleared
+ *     through unpoison_memory() (the hwpoison-inject debugfs interface).
+ *     Intended for testing the memory-failure pipeline; restricted to
+ *     CAP_SYS_ADMIN.
+ *   reversible: no
+ *
+ * side-effect: hardware
+ *   target: physical page (soft_offline_page)
+ *   condition: MADV_SOFT_OFFLINE
+ *   desc: Migrates the contents off the affected pages and removes them from
+ *     the buddy allocator. Page contents are preserved. madvise() has no
+ *     inverse. The pages can be brought back through unpoison_memory() (the
+ *     hwpoison-inject debugfs interface). Intended for testing the
+ *     memory-failure pipeline; restricted to CAP_SYS_ADMIN.
+ *   reversible: no
+ *
+ * side-effect: modify_state
+ *   target: KSM merge state (vm_flags & VM_MERGEABLE)
+ *   condition: MADV_MERGEABLE, MADV_UNMERGEABLE
+ *   desc: Toggles VM_MERGEABLE, the VMA's eligibility for the kernel
+ *     same-page merger. MADV_MERGEABLE lets ksmd later replace identical
+ *     anonymous pages with shared, write-protected copies and is silently
+ *     ignored on KSM-incompatible VMAs. MADV_UNMERGEABLE synchronously
+ *     calls break_ksm(), which faults each KSM page in the range back to an
+ *     exclusive copy, before clearing VM_MERGEABLE. An -ENOMEM from either
+ *     advice is returned as -EAGAIN. The flag toggle is reversible by
+ *     issuing the inverse advice.
+ *   reversible: yes
+ *
+ * side-effect: modify_state
+ *   target: userfaultfd event queue
+ *   condition: MADV_DONTNEED, MADV_DONTNEED_LOCKED, MADV_FREE, MADV_REMOVE
+ *     on a VMA whose userfaultfd context negotiated UFFD_FEATURE_EVENT_REMOVE
+ *   desc: Generates a UFFD_EVENT_REMOVE notification covering the discarded
+ *     range so userfaultfd monitors observing the mapping see the
+ *     invalidation. The event is queued before the discard takes effect; the
+ *     monitor cannot veto it.
+ *   reversible: no
+ *
+ * capability: CAP_SYS_ADMIN
+ *   type: perform_operation
+ *   allows: Inject memory errors via MADV_HWPOISON or MADV_SOFT_OFFLINE
+ *   without: Both behaviors return -EPERM
+ *   condition: Checked at entry to madvise_inject_error() before any pages
+ *     are looked up
+ *
+ * constraint: Page-aligned start
+ *   desc: start must lie on a page boundary; otherwise the call returns
+ *     -EINVAL before any VMA is consulted.
+ *   expr: (start & (PAGE_SIZE - 1)) == 0
+ *
+ * constraint: Length rounded up to PAGE_SIZE
+ *   desc: The effective range length is PAGE_ALIGN(len_in). A non-zero len_in
+ *     that overflows during rounding, or a (start, end) range that wraps,
+ *     is rejected with -EINVAL.
+ *   expr: end = start + PAGE_ALIGN(len_in); end >= start
+ *
+ * constraint: Behavior must be supported
+ *   desc: behavior must be one of the MADV_* values listed under the
+ *     behavior parameter. Behaviors gated by Kconfig (KSM, THP, memory
+ *     failure) are rejected with -EINVAL when the corresponding option is
+ *     disabled in the running kernel.
+ *
+ * constraint: mseal-protected discards
+ *   desc: On 64-bit kernels, a discard operation (FREE, DONTNEED,
+ *     DONTNEED_LOCKED, REMOVE, DONTFORK, WIPEONFORK, GUARD_INSTALL) against
+ *     a sealed anonymous VMA is rejected unless the mapping is currently
+ *     writable -- both VM_WRITE in vm_flags and arch_vma_access_permitted()
+ *     allowing write -- so that mseal(2) cannot be bypassed by instructing
+ *     the kernel to throw the data away. File-backed sealed VMAs and
+ *     writable sealed VMAs are not subject to this restriction.
+ *   expr: !is_discard(behavior) || !vma_is_sealed(vma) ||
+ *     !vma_is_anonymous(vma) || ((vma->vm_flags & VM_WRITE) &&
+ *     arch_vma_access_permitted(vma, true, false, false))
+ *
+ * constraint: MADV_FREE requires anonymous mappings
+ *   desc: MADV_FREE is defined only over anonymous mappings; the handler
+ *     requires vma_is_anonymous() (no vm_ops) and rejects file-backed VMAs,
+ *     including shared anonymous (shmem) VMAs, with -EINVAL.
+ *   expr: vma_is_anonymous(vma)
+ *
+ * constraint: MADV_WIPEONFORK requires private anonymous mappings
+ *   desc: MADV_WIPEONFORK rejects file-backed mappings and shared anonymous
+ *     mappings; only MAP_PRIVATE anonymous VMAs accept it. Both rejections
+ *     surface as -EINVAL.
+ *   expr: !vma->vm_file && !(vma->vm_flags & VM_SHARED)
+ *
+ * constraint: MADV_REMOVE requires a shared file mapping that may be written
+ *   desc: MADV_REMOVE rejects VM_LOCKED VMAs and VMAs without an associated
+ *     file/mapping/host inode with -EINVAL. It rejects VMAs failing
+ *     vma_is_shared_maywrite() (VM_SHARED and VM_MAYWRITE) with -EACCES,
+ *     which covers private file mappings and shared mappings of files not
+ *     opened for writing, but not a PROT_READ MAP_SHARED mapping of a file
+ *     opened O_RDWR.
+ *   expr: !(vma->vm_flags & VM_LOCKED) && vma->vm_file &&
+ *     vma->vm_file->f_mapping && vma->vm_file->f_mapping->host &&
+ *     vma_is_shared_maywrite(vma)
+ *
+ * constraint: MADV_COLD / MADV_PAGEOUT VMA filter
+ *   desc: Both behaviors require LRU-managed pages; they reject VMAs that
+ *     are mlocked, raw-PFN or hugetlb.
+ *   expr: !(vma->vm_flags & (VM_LOCKED | VM_PFNMAP | VM_HUGETLB))
+ *
+ * examples: madvise(p, len, MADV_SEQUENTIAL);  // set VM_SEQ_READ on the VMA
+ *   madvise(p, len, MADV_POPULATE_WRITE);  // prefault writable PTEs
+ *   madvise(p, len, MADV_DONTNEED);        // discard anonymous pages
+ *   madvise(p, len, MADV_GUARD_INSTALL);   // install SIGSEGV guard pages
+ *
+ * notes: Behavior introduction history (mainline): MADV_FREE in 4.5,
+ *   MADV_WIPEONFORK / MADV_KEEPONFORK in 4.14, MADV_COLD / MADV_PAGEOUT in
+ *   5.4, MADV_POPULATE_READ / MADV_POPULATE_WRITE in 5.14,
+ *   MADV_DONTNEED_LOCKED in 5.18, MADV_COLLAPSE in 6.1, MADV_GUARD_INSTALL /
+ *   MADV_GUARD_REMOVE in 6.13. Code that wants to remain portable to older
+ *   kernels must handle -EINVAL gracefully and fall back.
+ *
+ *   process_madvise(2) applies the same advice values to another process
+ *   identified by a pidfd. When the target mm is the caller's own (the
+ *   pidfd refers to the caller), any locally-supported MADV_* value is
+ *   accepted. When the target is a different mm, the behavior must be in
+ *   the non-destructive remote subset (MADV_COLD, MADV_PAGEOUT,
+ *   MADV_WILLNEED, MADV_COLLAPSE) or the call returns -EINVAL, and the
+ *   caller must hold CAP_SYS_NICE.
+ *
+ *   MADV_PAGEOUT on a non-anonymous VM_MAYSHARE mapping is a silent no-op
+ *   returning 0 unless can_do_file_pageout() holds, meaning the caller owns
+ *   the file or is capable over it (file_owner_or_capable(), which honors
+ *   the mount idmap) or may write it (file_permission(MAY_WRITE)). On a
+ *   private file mapping that fails the same test, only anonymous pages are
+ *   paged out. MADV_COLD has no such filter.
+ *
+ *   MADV_GUARD_INSTALL retries up to MAX_MADVISE_GUARD_RETRIES (3) times
+ *   when it loses races with concurrent faulting or khugepaged. If those
+ *   retries are exhausted the handler returns -ERESTARTNOINTR via
+ *   restart_syscall(), which sets TIF_SIGPENDING so the return path runs
+ *   signal handling. madvise() is then transparently re-executed with the
+ *   same arguments, after any handler for a pending signal has run, unless
+ *   a fatal signal terminates the task first. The caller never observes an
+ *   errno from the restart itself and the call appears to make eventual
+ *   forward progress.
+ *
+ *   anon_vma_prepare() failures inside MADV_GUARD_INSTALL bypass the
+ *   ENOMEM-to-EAGAIN translation that applies to the VMA-flag-mutating
+ *   behaviors and surface as -ENOMEM directly.
+ *
+ *   Architecture note: alpha defines MADV_DONTNEED as 6 (not 4) and reserves
+ *   MADV_SPACEAVAIL=5; portable code must use the symbolic names from
+ *   <sys/mman.h>.
+ */
 SYSCALL_DEFINE3(madvise, unsigned long, start, size_t, len_in, int, behavior)
 {
 	return do_madvise(current->mm, start, len_in, behavior);
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/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
+```
diff --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"));
+    }
+}
diff --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(&section);
+                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(&param_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(&section);
+                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(&section);
+                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(&["", "  "]), "");
+    }
+}
diff --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(())
+}
diff --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(&current_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(
+                                &current_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");
+    }
+}
diff --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));
+    }
+}
diff --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) = &param.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) = &param.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(())
+}
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()
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 273853937c254..d5f973eeefc83 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -60,6 +60,7 @@ TARGETS += intel_pstate
 TARGETS += iommu
 TARGETS += ipc
 TARGETS += ir
+TARGETS += kapi
 TARGETS += kcmp
 TARGETS += kexec
 TARGETS += kselftest_harness
diff --git a/tools/testing/selftests/kapi/.gitignore b/tools/testing/selftests/kapi/.gitignore
new file mode 100644
index 0000000000000..e60893d183d81
--- /dev/null
+++ b/tools/testing/selftests/kapi/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+test_kapi
diff --git a/tools/testing/selftests/kapi/Makefile b/tools/testing/selftests/kapi/Makefile
new file mode 100644
index 0000000000000..d1bb26541c0b8
--- /dev/null
+++ b/tools/testing/selftests/kapi/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_GEN_PROGS := test_kapi
+
+CFLAGS += -static -Wall -Wextra -O2 $(KHDR_INCLUDES)
+
+include ../lib.mk
diff --git a/tools/testing/selftests/kapi/kapi_test_util.h b/tools/testing/selftests/kapi/kapi_test_util.h
new file mode 100644
index 0000000000000..a09871207bfc6
--- /dev/null
+++ b/tools/testing/selftests/kapi/kapi_test_util.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Compatibility helpers for KAPI selftests.
+ *
+ * __NR_open does not exist on architectures using the generic syscall table
+ * (e.g. arm64, riscv); only __NR_openat does. There the wrapper falls back to
+ * openat(AT_FDCWD, ...), which does not go through sys_open, so the sys_open
+ * KAPI checks are not exercised.
+ */
+#ifndef KAPI_TEST_UTIL_H
+#define KAPI_TEST_UTIL_H
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/syscall.h>
+
+#ifndef __NR_open
+static inline long kapi_sys_open(const char *pathname, int flags, int mode)
+{
+	return syscall(__NR_openat, AT_FDCWD, pathname, flags, mode);
+}
+#else
+static inline long kapi_sys_open(const char *pathname, int flags, int mode)
+{
+	return syscall(__NR_open, pathname, flags, mode);
+}
+#endif
+
+#endif /* KAPI_TEST_UTIL_H */
diff --git a/tools/testing/selftests/kapi/test_kapi.c b/tools/testing/selftests/kapi/test_kapi.c
new file mode 100644
index 0000000000000..deccb075ee1f9
--- /dev/null
+++ b/tools/testing/selftests/kapi/test_kapi.c
@@ -0,0 +1,1163 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Userspace selftest for KAPI runtime verification of syscall parameters.
+ *
+ * Exercises sys_open, sys_read, sys_write, and sys_close through raw
+ * syscall() to ensure KAPI pre-validation wrappers interact correctly
+ * with normal kernel error handling.
+ *
+ * Requires CONFIG_KAPI_RUNTIME_CHECKS=y for full coverage; many tests
+ * also pass without it.
+ *
+ * TAP output format.
+ */
+
+#define _GNU_SOURCE
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <signal.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <linux/limits.h>
+#include "../kselftest.h"
+#include "kapi_test_util.h"
+
+#define NUM_TESTS 31
+
+/* Set from the SIGPIPE handler. */
+static volatile sig_atomic_t got_sigpipe;
+
+static void tap_ok(const char *desc)
+{
+	ksft_test_result_pass("%s\n", desc);
+}
+
+static void tap_fail(const char *desc, const char *reason)
+{
+	ksft_test_result_fail("%s: %s\n", desc, reason);
+}
+
+static void tap_skip(const char *desc, const char *reason)
+{
+	ksft_test_result_skip("%s: %s\n", desc, reason);
+}
+
+/*
+ * Return true when CONFIG_KAPI_RUNTIME_CHECKS is active: it rejects unknown
+ * open flag bits with EINVAL, while the kernel otherwise ignores them.
+ */
+static bool kapi_runtime_checks_active(void)
+{
+	long ret;
+
+	errno = 0;
+	ret = kapi_sys_open("/dev/null", 0x10000000, 0);
+	if (ret >= 0) {
+		syscall(__NR_close, (int)ret);
+		return false;
+	}
+
+	return errno == EINVAL;
+}
+
+static void sigpipe_handler(int sig)
+{
+	(void)sig;
+	got_sigpipe = 1;
+}
+
+/* ---- Valid operation tests ---- */
+
+/*
+ * Test 1: open a readable file
+ * Returns fd on success.
+ */
+static int test_open_valid(void)
+{
+	errno = 0;
+	long fd = kapi_sys_open("/etc/hostname", O_RDONLY, 0);
+
+	if (fd >= 0) {
+		tap_ok("open valid file");
+	} else {
+		/* /etc/hostname might not exist; try /etc/passwd */
+		errno = 0;
+		fd = kapi_sys_open("/etc/passwd", O_RDONLY, 0);
+		if (fd >= 0)
+			tap_ok("open valid file (fallback /etc/passwd)");
+		else
+			tap_fail("open valid file", strerror(errno));
+	}
+	return (int)fd;
+}
+
+/*
+ * Test 2: read from fd
+ */
+static void test_read_valid(int fd)
+{
+	char buf[256];
+
+	errno = 0;
+	long ret = syscall(__NR_read, fd, buf, sizeof(buf));
+
+	if (ret > 0)
+		tap_ok("read from valid fd");
+	else if (ret == 0)
+		tap_ok("read from valid fd (EOF)");
+	else
+		tap_fail("read from valid fd", strerror(errno));
+}
+
+/*
+ * Test 3: write to /dev/null
+ */
+static void test_write_valid(void)
+{
+	errno = 0;
+	long devnull = kapi_sys_open("/dev/null", O_WRONLY, 0);
+
+	if (devnull < 0) {
+		tap_fail("write to /dev/null (open failed)", strerror(errno));
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)devnull, "hello", 5);
+
+	if (ret == 5)
+		tap_ok("write to /dev/null");
+	else
+		tap_fail("write to /dev/null",
+			 ret < 0 ? strerror(errno) : "short write");
+
+	syscall(__NR_close, (int)devnull);
+}
+
+/*
+ * Test 4: close fd
+ */
+static void test_close_valid(int fd)
+{
+	errno = 0;
+	long ret = syscall(__NR_close, fd);
+
+	if (ret == 0)
+		tap_ok("close valid fd");
+	else
+		tap_fail("close valid fd", strerror(errno));
+}
+
+/* ---- KAPI parameter rejection tests ---- */
+
+/*
+ * Test 5: open with invalid flag bits
+ * 0x10000000 is outside the valid O_* mask, KAPI should reject.
+ */
+static void test_open_invalid_flags(void)
+{
+	long ret;
+
+	if (!kapi_runtime_checks_active()) {
+		tap_skip("open with invalid flags",
+			 "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+		return;
+	}
+
+	errno = 0;
+	/*
+	 * Use /dev/null (always present on any sane rootfs) so KAPI's flag
+	 * validation is reached before a path-lookup ENOENT can mask it.
+	 * 0x10000000 is outside the valid O_* mask.
+	 */
+	ret = kapi_sys_open("/dev/null", 0x10000000, 0);
+
+	if (ret == -1 && errno == EINVAL) {
+		tap_ok("open with invalid flags returns EINVAL");
+	} else if (ret >= 0) {
+		tap_fail("open with invalid flags", "expected EINVAL, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+			 strerror(errno));
+		tap_fail("open with invalid flags", msg);
+	}
+}
+
+/*
+ * Test 6: open with mode bits above S_IALLUGO
+ * The kernel masks mode with S_IALLUGO (07777), so KAPI must accept it.
+ */
+static void test_open_high_mode_bits(void)
+{
+	const char *desc = "open with mode bits above 07777 creates file";
+	char path[64];
+	struct stat st;
+	mode_t um, want;
+	long fd;
+
+	snprintf(path, sizeof(path), "/tmp/kapi_test_mode.%d", getpid());
+	unlink(path);
+
+	um = umask(0);
+	umask(um);
+	want = 07777 & ~um;
+
+	errno = 0;
+	fd = kapi_sys_open(path, O_CREAT | O_WRONLY | O_EXCL, 0xFFFF);
+	if (fd < 0) {
+		tap_fail(desc, strerror(errno));
+		return;
+	}
+
+	if (fstat((int)fd, &st) < 0) {
+		tap_fail(desc, strerror(errno));
+	} else if ((st.st_mode & 07777) != want) {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected mode %04o, got %04o",
+			 (unsigned int)want, (unsigned int)(st.st_mode & 07777));
+		tap_fail(desc, msg);
+	} else {
+		tap_ok(desc);
+	}
+
+	syscall(__NR_close, (int)fd);
+	unlink(path);
+}
+
+/*
+ * Test 7: open with NULL path
+ * KAPI USER_PATH constraint should reject NULL.
+ */
+static void test_open_null_path(void)
+{
+	errno = 0;
+	long ret = kapi_sys_open(NULL, O_RDONLY, 0);
+
+	if (ret == -1 && errno == EINVAL) {
+		tap_ok("open with NULL path returns EINVAL");
+	} else if (ret == -1 && errno == EFAULT) {
+		/* Kernel may catch this as EFAULT before KAPI */
+		tap_ok("open with NULL path returns EFAULT (acceptable)");
+	} else if (ret >= 0) {
+		tap_fail("open with NULL path", "expected error, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "got %s", strerror(errno));
+		tap_fail("open with NULL path", msg);
+	}
+}
+
+/*
+ * Test 8: open with flag bit 30 set (0x40000000)
+ * This bit is outside the valid O_* mask, KAPI should reject with EINVAL.
+ */
+static void test_open_flag_bit30(void)
+{
+	long ret;
+
+	if (!kapi_runtime_checks_active()) {
+		tap_skip("open with flag bit 30 (0x40000000) returns EINVAL",
+			 "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+		return;
+	}
+
+	errno = 0;
+	ret = kapi_sys_open("/dev/null", 0x40000000, 0);
+
+	if (ret == -1 && errno == EINVAL) {
+		tap_ok("open with flag bit 30 (0x40000000) returns EINVAL");
+	} else if (ret >= 0) {
+		tap_fail("open with flag bit 30 (0x40000000) returns EINVAL",
+			 "expected EINVAL, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+			 strerror(errno));
+		tap_fail("open with flag bit 30 (0x40000000) returns EINVAL",
+			 msg);
+	}
+}
+
+/* ---- Boundary condition and error path tests ---- */
+
+/*
+ * Test 9: read with fd=-1 should return an error.
+ * With CONFIG_KAPI_RUNTIME_CHECKS=y, KAPI validates the fd first and
+ * rejects negative fds (other than AT_FDCWD) with EINVAL.  Without
+ * KAPI, the kernel returns EBADF.  Accept either.
+ */
+static void test_read_bad_fd(void)
+{
+	char buf[16];
+
+	errno = 0;
+	long ret = syscall(__NR_read, -1, buf, sizeof(buf));
+
+	if (ret == -1 && (errno == EBADF || errno == EINVAL)) {
+		tap_ok("read with fd=-1 returns error");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF/EINVAL, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("read with fd=-1 returns error", msg);
+	}
+}
+
+/*
+ * Test 10: read with count=0 should return 0
+ */
+static void test_read_zero_count(void)
+{
+	char buf[1];
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("read with count=0 returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_read, (int)fd, buf, 0);
+
+	if (ret == 0) {
+		tap_ok("read with count=0 returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("read with count=0 returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 11: write with count=0 should return 0
+ */
+static void test_write_zero_count(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("write with count=0 returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, "x", 0);
+
+	if (ret == 0) {
+		tap_ok("write with count=0 returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("write with count=0 returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 12: open with a path longer than PATH_MAX should fail
+ * Expect ENAMETOOLONG or EINVAL.
+ */
+static void test_open_long_path(void)
+{
+	char *longpath;
+	size_t len = PATH_MAX + 256;
+
+	longpath = malloc(len);
+	if (!longpath) {
+		tap_fail("open with path > PATH_MAX", "malloc failed");
+		return;
+	}
+
+	memset(longpath, 'A', len - 1);
+	longpath[0] = '/';
+	longpath[len - 1] = '\0';
+
+	errno = 0;
+	long ret = kapi_sys_open(longpath, O_RDONLY, 0);
+
+	if (ret == -1 && (errno == ENAMETOOLONG || errno == EINVAL)) {
+		tap_ok("open with path > PATH_MAX returns ENAMETOOLONG/EINVAL");
+	} else if (ret >= 0) {
+		tap_fail("open with path > PATH_MAX",
+			 "expected error, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg),
+			 "expected ENAMETOOLONG/EINVAL, got %s",
+			 strerror(errno));
+		tap_fail("open with path > PATH_MAX", msg);
+	}
+
+	free(longpath);
+}
+
+/*
+ * Test 13: read with unmapped user pointer should return EFAULT or EINVAL.
+ * Use a pipe with data so the kernel actually tries to copy to the buffer.
+ */
+static void test_read_unmapped_buf(void)
+{
+	int pipefd[2];
+
+	if (pipe(pipefd) < 0) {
+		tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+			 "pipe() failed");
+		return;
+	}
+
+	/* Write some data so read has something to copy */
+	if (write(pipefd[1], "hello", 5) != 5) {
+		tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+			 "pipe write failed");
+		close(pipefd[0]);
+		close(pipefd[1]);
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_read, pipefd[0], (void *)0xDEAD0000, 16);
+
+	if (ret == -1 && (errno == EFAULT || errno == EINVAL)) {
+		tap_ok("read with unmapped buffer returns EFAULT/EINVAL");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg),
+			 "expected EFAULT/EINVAL, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+			 msg);
+	}
+
+	close(pipefd[0]);
+	close(pipefd[1]);
+}
+
+/*
+ * Test 14: write with unmapped user pointer should return EFAULT or EINVAL.
+ * Use a pipe so the kernel actually tries to copy from the buffer.
+ */
+static void test_write_unmapped_buf(void)
+{
+	int pipefd[2];
+
+	if (pipe(pipefd) < 0) {
+		tap_fail("write with unmapped buffer returns EFAULT/EINVAL",
+			 "pipe() failed");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, pipefd[1], (void *)0xDEAD0000, 16);
+
+	if (ret == -1 && (errno == EFAULT || errno == EINVAL)) {
+		tap_ok("write with unmapped buffer returns EFAULT/EINVAL");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg),
+			 "expected EFAULT/EINVAL, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("write with unmapped buffer returns EFAULT/EINVAL",
+			 msg);
+	}
+
+	close(pipefd[0]);
+	close(pipefd[1]);
+}
+
+/*
+ * Test 15: close an already-closed fd should return EBADF
+ */
+static void test_close_already_closed(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("close already-closed fd returns EBADF",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	/* Close it once - should succeed */
+	syscall(__NR_close, (int)fd);
+
+	/* Close it again - should fail with EBADF */
+	errno = 0;
+	long ret = syscall(__NR_close, (int)fd);
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("close already-closed fd returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret == 0 ? "success" : strerror(errno));
+		tap_fail("close already-closed fd returns EBADF", msg);
+	}
+}
+
+/*
+ * Test 16: open /dev/null with O_RDONLY|O_CLOEXEC should succeed
+ */
+static void test_open_valid_cloexec(void)
+{
+	errno = 0;
+	long fd = kapi_sys_open("/dev/null", O_RDONLY | O_CLOEXEC, 0);
+
+	if (fd >= 0) {
+		tap_ok("open /dev/null with O_RDONLY|O_CLOEXEC succeeds");
+		syscall(__NR_close, (int)fd);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected success, got %s",
+			 strerror(errno));
+		tap_fail("open /dev/null with O_RDONLY|O_CLOEXEC succeeds",
+			 msg);
+	}
+}
+
+/*
+ * Test 17: write 0 bytes to /dev/null should return 0
+ */
+static void test_write_zero_devnull(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("write 0 bytes to /dev/null returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, "", 0);
+
+	if (ret == 0) {
+		tap_ok("write 0 bytes to /dev/null returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("write 0 bytes to /dev/null returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 18: read from a write-only fd should return EBADF
+ */
+static void test_read_writeonly_fd(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("read from write-only fd returns EBADF",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	char buf[16];
+
+	errno = 0;
+	long ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("read from write-only fd returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("read from write-only fd returns EBADF", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 19: write to a read-only fd should return EBADF
+ */
+static void test_write_readonly_fd(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("write to read-only fd returns EBADF",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, "hello", 5);
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("write to read-only fd returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("write to read-only fd returns EBADF", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 20: close fd 9999 (likely invalid) should return EBADF
+ */
+static void test_close_fd_9999(void)
+{
+	errno = 0;
+	long ret = syscall(__NR_close, 9999);
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("close fd 9999 returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret == 0 ? "success" : strerror(errno));
+		tap_fail("close fd 9999 returns EBADF", msg);
+	}
+}
+
+/*
+ * Test 21: read from pipe after write end is closed returns 0 (EOF)
+ */
+static void test_read_closed_pipe(void)
+{
+	int pipefd[2];
+
+	if (pipe(pipefd) < 0) {
+		tap_fail("read from closed pipe returns 0 (EOF)",
+			 "pipe() failed");
+		return;
+	}
+
+	/* Close write end */
+	close(pipefd[1]);
+
+	char buf[16];
+
+	errno = 0;
+	long ret = syscall(__NR_read, pipefd[0], buf, sizeof(buf));
+
+	if (ret == 0) {
+		tap_ok("read from closed pipe returns 0 (EOF)");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, ret < 0 ? strerror(errno) : "n/a");
+		tap_fail("read from closed pipe returns 0 (EOF)", msg);
+	}
+
+	close(pipefd[0]);
+}
+
+/*
+ * Test 22: write to pipe after read end is closed returns EPIPE + SIGPIPE
+ */
+static void test_write_closed_pipe(void)
+{
+	int pipefd[2];
+	struct sigaction sa, old_sa;
+
+	if (pipe(pipefd) < 0) {
+		tap_fail("write to closed pipe returns EPIPE + SIGPIPE",
+			 "pipe() failed");
+		return;
+	}
+
+	/* Install SIGPIPE handler */
+	memset(&sa, 0, sizeof(sa));
+	sa.sa_handler = sigpipe_handler;
+	sigemptyset(&sa.sa_mask);
+	sigaction(SIGPIPE, &sa, &old_sa);
+
+	got_sigpipe = 0;
+
+	/* Close read end */
+	close(pipefd[0]);
+
+	errno = 0;
+	long ret = syscall(__NR_write, pipefd[1], "hello", 5);
+
+	if (ret == -1 && errno == EPIPE && got_sigpipe) {
+		tap_ok("write to closed pipe returns EPIPE + SIGPIPE");
+	} else if (ret == -1 && errno == EPIPE) {
+		tap_ok("write to closed pipe returns EPIPE (SIGPIPE not caught)");
+	} else {
+		char msg[128];
+
+		snprintf(msg, sizeof(msg),
+			 "expected EPIPE, got %s (sigpipe=%d)",
+			 ret >= 0 ? "success" : strerror(errno),
+			 (int)got_sigpipe);
+		tap_fail("write to closed pipe returns EPIPE + SIGPIPE", msg);
+	}
+
+	/* Restore SIGPIPE handler */
+	sigaction(SIGPIPE, &old_sa, NULL);
+	close(pipefd[1]);
+}
+
+/*
+ * Test 23: open with O_DIRECTORY on a non-directory returns ENOTDIR
+ */
+static void test_open_directory_on_file(void)
+{
+	errno = 0;
+	long ret = kapi_sys_open("/dev/null", O_RDONLY | O_DIRECTORY, 0);
+
+	if (ret == -1 && errno == ENOTDIR) {
+		tap_ok("open O_DIRECTORY on non-directory returns ENOTDIR");
+	} else if (ret >= 0) {
+		tap_fail("open O_DIRECTORY on non-directory",
+			 "expected ENOTDIR, got success");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected ENOTDIR, got %s",
+			 strerror(errno));
+		tap_fail("open O_DIRECTORY on non-directory", msg);
+	}
+}
+
+/*
+ * Test 24: open nonexistent file without O_CREAT returns ENOENT
+ */
+static void test_open_nonexistent(void)
+{
+	errno = 0;
+	long ret = kapi_sys_open("/tmp/kapi_nonexistent_file_12345",
+				 O_RDONLY, 0);
+
+	if (ret == -1 && errno == ENOENT) {
+		tap_ok("open nonexistent file without O_CREAT returns ENOENT");
+	} else if (ret >= 0) {
+		tap_fail("open nonexistent file",
+			 "expected ENOENT, got success (file exists?)");
+		syscall(__NR_close, (int)ret);
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected ENOENT, got %s",
+			 strerror(errno));
+		tap_fail("open nonexistent file", msg);
+	}
+}
+
+/*
+ * Test 25: close stdin (fd 0) should succeed
+ * We dup it first so we can restore it.
+ */
+static void test_close_stdin(void)
+{
+	int saved_stdin = dup(0);
+
+	if (saved_stdin < 0) {
+		tap_fail("close stdin succeeds", "cannot dup stdin");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_close, 0);
+
+	if (ret == 0) {
+		tap_ok("close stdin (fd 0) succeeds");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected success, got %s",
+			 strerror(errno));
+		tap_fail("close stdin (fd 0) succeeds", msg);
+	}
+
+	/* Restore stdin */
+	dup2(saved_stdin, 0);
+	close(saved_stdin);
+}
+
+/*
+ * Test 26: read after close returns EBADF
+ */
+static void test_read_after_close(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("read after close returns EBADF",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	syscall(__NR_close, (int)fd);
+
+	char buf[16];
+
+	errno = 0;
+	long ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));
+
+	if (ret == -1 && errno == EBADF) {
+		tap_ok("read after close returns EBADF");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+			 ret >= 0 ? "success" : strerror(errno));
+		tap_fail("read after close returns EBADF", msg);
+	}
+}
+
+/*
+ * Test 27: write with large count
+ * Without KAPI: the kernel clamps count to MAX_RW_COUNT and succeeds.
+ * With KAPI: KAPI validates the buffer against the count and may
+ * return EFAULT/EINVAL since the buffer is smaller than count.
+ * Accept either success or EFAULT/EINVAL.
+ */
+static void test_write_large_count(void)
+{
+	long fd;
+	char buf[64] = "test data";
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("write with large count handled correctly",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, buf, (size_t)0x7ffff000UL);
+
+	if (ret > 0) {
+		tap_ok("write with large count succeeds (clamped, no KAPI)");
+	} else if (ret == -1 && (errno == EFAULT || errno == EINVAL)) {
+		tap_ok("write with large count returns EFAULT/EINVAL (KAPI validates buffer)");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected success or EFAULT, got %s",
+			 ret == 0 ? "zero" : strerror(errno));
+		tap_fail("write with large count handled correctly", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/* ---- Integration tests ---- */
+
+/*
+ * Test 28: full normal syscall path - open, read, write, close
+ * Verify KAPI does not interfere with normal operations.
+ */
+static void test_normal_path(void)
+{
+	long rd_fd, wr_fd;
+	char buf[128];
+	int ok = 1;
+	char reason[128] = "";
+
+	/* Open a readable file */
+	errno = 0;
+	rd_fd = kapi_sys_open("/etc/hostname", O_RDONLY, 0);
+	if (rd_fd < 0) {
+		errno = 0;
+		rd_fd = kapi_sys_open("/etc/passwd", O_RDONLY, 0);
+	}
+	if (rd_fd < 0) {
+		snprintf(reason, sizeof(reason), "open readable file: %s",
+			 strerror(errno));
+		ok = 0;
+	}
+
+	/* Read from it */
+	if (ok) {
+		errno = 0;
+		long n = syscall(__NR_read, (int)rd_fd, buf, sizeof(buf));
+
+		if (n < 0) {
+			snprintf(reason, sizeof(reason), "read: %s",
+				 strerror(errno));
+			ok = 0;
+		}
+	}
+
+	/* Open /dev/null for writing */
+	wr_fd = -1;
+	if (ok) {
+		errno = 0;
+		wr_fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+		if (wr_fd < 0) {
+			snprintf(reason, sizeof(reason),
+				 "open /dev/null: %s", strerror(errno));
+			ok = 0;
+		}
+	}
+
+	/* Write to /dev/null */
+	if (ok) {
+		errno = 0;
+		long n = syscall(__NR_write, (int)wr_fd, "test", 4);
+
+		if (n != 4) {
+			snprintf(reason, sizeof(reason), "write: %s",
+				 n < 0 ? strerror(errno) : "short write");
+			ok = 0;
+		}
+	}
+
+	/* Close both fds */
+	if (rd_fd >= 0) {
+		errno = 0;
+		if (syscall(__NR_close, (int)rd_fd) != 0 && ok) {
+			snprintf(reason, sizeof(reason), "close read fd: %s",
+				 strerror(errno));
+			ok = 0;
+		}
+	}
+
+	if (wr_fd >= 0) {
+		errno = 0;
+		if (syscall(__NR_close, (int)wr_fd) != 0 && ok) {
+			snprintf(reason, sizeof(reason), "close write fd: %s",
+				 strerror(errno));
+			ok = 0;
+		}
+	}
+
+	if (ok)
+		tap_ok("normal syscall path (open/read/write/close) works");
+	else
+		tap_fail("normal syscall path (open/read/write/close) works",
+			 reason);
+}
+
+/*
+ * Test 29: read with a NULL buffer and count=0 should return 0
+ * No access is performed, so KAPI must not reject the NULL pointer.
+ */
+static void test_read_null_zero_count(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+	if (fd < 0) {
+		tap_fail("read with NULL buffer and count=0 returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_read, (int)fd, NULL, 0);
+
+	if (ret == 0) {
+		tap_ok("read with NULL buffer and count=0 returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("read with NULL buffer and count=0 returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 30: write with a NULL buffer and count=0 should return 0
+ * No access is performed, so KAPI must not reject the NULL pointer.
+ */
+static void test_write_null_zero_count(void)
+{
+	long fd;
+
+	errno = 0;
+	fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+	if (fd < 0) {
+		tap_fail("write with NULL buffer and count=0 returns 0",
+			 "cannot open /dev/null");
+		return;
+	}
+
+	errno = 0;
+	long ret = syscall(__NR_write, (int)fd, NULL, 0);
+
+	if (ret == 0) {
+		tap_ok("write with NULL buffer and count=0 returns 0");
+	} else {
+		char msg[64];
+
+		snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+			 ret, strerror(errno));
+		tap_fail("write with NULL buffer and count=0 returns 0", msg);
+	}
+
+	syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 31: verify dmesg contains KAPI warnings for the invalid tests
+ */
+static void test_dmesg_warnings(void)
+{
+	int kmsg_fd;
+
+	if (!kapi_runtime_checks_active()) {
+		tap_skip("dmesg contains expected KAPI warnings",
+			 "CONFIG_KAPI_RUNTIME_CHECKS not enabled");
+		return;
+	}
+
+	kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+	if (kmsg_fd < 0) {
+		tap_skip("dmesg contains expected KAPI warnings",
+			 "cannot open /dev/kmsg");
+		return;
+	}
+
+	char line[4096];
+	int found_invalid_bits = 0;
+	int found_null = 0;
+	ssize_t n;
+
+	for (;;) {
+		n = read(kmsg_fd, line, sizeof(line) - 1);
+		if (n > 0) {
+			line[n] = '\0';
+			if (strstr(line, "contains invalid bits"))
+				found_invalid_bits++;
+			if (strstr(line, "NULL") && strstr(line, "not allowed"))
+				found_null++;
+		} else if (n == -1 && errno == EPIPE) {
+			/* Ring buffer wrapped, continue reading */
+			continue;
+		} else {
+			/* EAGAIN (no more messages) or other error */
+			break;
+		}
+	}
+
+	close(kmsg_fd);
+
+	if (found_invalid_bits >= 2 && found_null >= 1) {
+		tap_ok("dmesg contains expected KAPI warnings");
+	} else {
+		char msg[128];
+
+		snprintf(msg, sizeof(msg),
+			 "expected invalid_bits>=2 null>=1, got invalid_bits=%d null=%d",
+			 found_invalid_bits, found_null);
+		tap_fail("dmesg contains expected KAPI warnings", msg);
+	}
+}
+
+int main(void)
+{
+	ksft_print_header();
+	ksft_set_plan(NUM_TESTS);
+
+	/* Valid operations (1-4) */
+	int fd = test_open_valid();
+
+	if (fd >= 0)
+		test_read_valid(fd);
+	else
+		tap_fail("read from valid fd", "no fd from open");
+
+	test_write_valid();
+
+	if (fd >= 0)
+		test_close_valid(fd);
+	else
+		tap_fail("close valid fd", "no fd from open");
+
+	/* KAPI parameter validation (5-8) */
+	test_open_invalid_flags();
+	test_open_high_mode_bits();
+	test_open_null_path();
+	test_open_flag_bit30();
+
+	/* Boundary conditions and error paths (9-20) */
+	test_read_bad_fd();
+	test_read_zero_count();
+	test_write_zero_count();
+	test_open_long_path();
+	test_read_unmapped_buf();
+	test_write_unmapped_buf();
+	test_close_already_closed();
+	test_open_valid_cloexec();
+	test_write_zero_devnull();
+	test_read_writeonly_fd();
+	test_write_readonly_fd();
+	test_close_fd_9999();
+
+	/* Pipe and lifecycle tests (21-27) */
+	test_read_closed_pipe();
+	test_write_closed_pipe();
+	test_open_directory_on_file();
+	test_open_nonexistent();
+	test_close_stdin();
+	test_read_after_close();
+	test_write_large_count();
+
+	/* Integration (28-31) */
+	test_normal_path();
+	test_read_null_zero_count();
+	test_write_null_zero_count();
+	test_dmesg_warnings();
+
+	ksft_finished();
+	return 0;
+}

25/3 2026/10/08 12:36 llm kmsan-evaluator
1m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 250768
							output: 53
							thoughts: 2830
						

Thoughts:
**KMSAN Fuzzing Justification: A Detailed Analysis**

Alright, let's break this down. The request is to determine if this KAPI patch series warrants a dedicated KMSAN fuzzing session. My initial impression is that the answer is likely no, but we need to be thorough.

First, this patch introduces a "Kernel API Specification Framework (KAPI)". The core of the functionality seems to revolve around defining and validating syscall parameters and return values against specifications. The presence of runtime checks (`CONFIG_KAPI_RUNTIME_CHECKS`) is a key area to scrutinize.

The biggest question is: does this introduce any opportunities for uninitialized memory reads or information leaks? KMSAN is our primary tool here. The key files to review are where the KAPI code interacts with syscall handling and user-space data.

*   `arch/x86/include/asm/syscall_wrapper.h` and `include/linux/syscalls.h` are crucial. The `__KAPI_SYSCALL_DEFINEx` macro is where the syscall validation happens. Specifically, the line `s64 __params[x] = { __MAP(x, __SC_CAST_TO_S64, __VA_ARGS__) };` is interesting but, importantly, the actual parameters are copied by value from the stack. Any reads of kernel memory from within the KAPI calls will require explicit validation, and they don't seem to be present.

*   `kapi_validate_syscall_params` is the next target. It checks the syscall parameters against constraints defined in a `kernel_api_spec`. But crucially, does it read uninitialized memory from kernel space? The code does not. The parameters are passed in as `s64` which is initialized.

*   `kapi_debugfs.c` provides debugfs access to the KAPI specifications, and, therefore, data that can be potentially exported to user space. It uses `seq_file` and calls `kapi_export_json`. This writes to a user-provided buffer, but uses `scnprintf`. It's not copying raw kernel structures. The code appears safe here as well.

*   `trace_kapi_syscall_enter` and `trace_kapi_syscall_exit` in `include/trace/events/kapi.h` is another area of concern but `memset` appears to clear the trace arguments.

*   The allocation of API spec structures using `kzalloc_obj` with `kapi_register_spec`, and `kapi_kunit.c` with `kunit_kzalloc` or `kzalloc_obj`, and `memset`, indicates that any newly allocated memory is correctly initialized, so it doesn't appear we have any problems here.

*   The `kapi_validate_path` and `kapi_validate_user_ptr_with_params` functions are checking if the user pointers are in valid memory space. `strnlen_user` and `access_ok` are only reading user memory, and should not be a concern.

*   Finally, the static specs are critical. They are defined using `DEFINE_KERNEL_API_SPEC`, which ensures the structures are placed in `.rodata` and `.kapi_specs` and zero initialized. This is a very important point! Global/static const structures that aren't explicitly initialized in C are zeroed by default. Therefore, these structs are fully initialized, and we are safe here.

*   Reviewing the `__KAPI_SYSCALL_DEFINEx` macro again, the arguments are passed into the wrapped syscall functions, not kernel structures, which should reduce the risk of a leak.

The KMSAN considerations for needing to be enabled are:
1.  Kernel structures sent to user space where fields or structure padding might not be fully initialized/zeroed. - **Not present**
2.  Conditional logic or branching that depends on potentially uninitialized variables or struct fields. - **Not present**
3.  Allocation or initialization of complex data structures where uninitialized fields could be read later in reachable code paths. - **Not present**
4.  Bounds checks, lengths, or logic in a way that allows surrounding code to access uninitialized bytes of existing buffers. - **Not present**

Given the extensive review, it doesn't seem that this patch introduces new opportunities for uninitialized memory reads or information leaks. I conclude that running a dedicated KMSAN fuzzing session is likely unnecessary.



26/3 2026/10/08 12:37 tool grepper
0m Args:
{
  "Expression": "SYSCALL_DEFINE0",
  "PathPrefixes": [
    "include/linux/syscalls.h",
    "arch/x86/include/asm/syscall_wrapper.h"
  ]
}

Results:
{
  "Output": "arch/x86/include/asm/syscall_wrapper.h=12=extern long __ia32_sys_ni_syscall(const struct pt_regs *regs);\n--\narch/x86/include/asm/syscall_wrapper.h-193- */\narch/x86/include/asm/syscall_wrapper.h:194:#define COMPAT_SYSCALL_DEFINE0(name)\t\t\t\t\t\\\narch/x86/include/asm/syscall_wrapper.h-195-\tstatic long\t\t\t\t\t\t\t\\\n--\narch/x86/include/asm/syscall_wrapper.h-238-/*\narch/x86/include/asm/syscall_wrapper.h:239: * As the generic SYSCALL_DEFINE0() macro does not decode any parameters for\narch/x86/include/asm/syscall_wrapper.h-240- * obvious reasons, and passing struct pt_regs *regs to it in %rdi does not\n--\narch/x86/include/asm/syscall_wrapper.h-244- */\narch/x86/include/asm/syscall_wrapper.h:245:#define SYSCALL_DEFINE0(sname)\t\t\t\t\t\t\\\narch/x86/include/asm/syscall_wrapper.h-246-\tSYSCALL_METADATA(_##sname, 0);\t\t\t\t\t\\\n--\ninclude/linux/syscalls.h=81=struct file_attr;\n--\ninclude/linux/syscalls.h-103- * It may be useful for an architecture to override the definitions of the\ninclude/linux/syscalls.h:104: * SYSCALL_DEFINE0() and __SYSCALL_DEFINEx() macros, in particular to use a\ninclude/linux/syscalls.h-105- * different calling convention for syscalls. To allow for that, the prototypes\n--\ninclude/linux/syscalls.h=215=static inline int is_syscall_trace_event(struct trace_event_call *tp_event)\n--\ninclude/linux/syscalls.h-220-\ninclude/linux/syscalls.h:221:#ifndef SYSCALL_DEFINE0\ninclude/linux/syscalls.h:222:#define SYSCALL_DEFINE0(sname)\t\t\t\t\t\\\ninclude/linux/syscalls.h-223-\tSYSCALL_METADATA(_##sname, 0);\t\t\t\t\\\n--\ninclude/linux/syscalls.h-226-\tasmlinkage long sys_##sname(void)\ninclude/linux/syscalls.h:227:#endif /* SYSCALL_DEFINE0 */\ninclude/linux/syscalls.h-228-\n--\ninclude/linux/syscalls.h-318-#ifdef CONFIG_COMPAT\ninclude/linux/syscalls.h:319:#define SYSCALL32_DEFINE0 COMPAT_SYSCALL_DEFINE0\ninclude/linux/syscalls.h-320-#define SYSCALL32_DEFINE1 COMPAT_SYSCALL_DEFINE1\n--\ninclude/linux/syscalls.h-326-#else\ninclude/linux/syscalls.h:327:#define SYSCALL32_DEFINE0 SYSCALL_DEFINE0\ninclude/linux/syscalls.h-328-#define SYSCALL32_DEFINE1 SYSCALL_DEFINE1\n"
}

27/3 2026/10/08 12:37 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 255421
							output: 383
							thoughts: 3892
						

Thoughts:
**KAPI Validation: No KMSAN Needed**

Okay, so I've dug into this KAPI change pretty thoroughly. My understanding is that the core of this introduces runtime validation of syscall parameters based on statically defined specifications and adds documentation via kerneldoc comments. It does this without directly exposing any uninitialized kernel memory to userspace. My examination has led me to believe that there's no reason to flag this with `NeedsKMSAN=true`.

First, the code defines syscalls with `SYSCALL_DEFINE*` macro, but it’s the `__KAPI_SYSCALL_DEFINEx` macro that does the KAPI runtime validation via `kapi_validate_syscall_params`.  This function uses a `kernel_api_spec` structure that is either static const data in `.rodata`, or it's dynamically allocated and zeroed by `kzalloc_obj`, or `kunit_kzalloc` in tests. The validation checks the count and values of syscall parameters against the spec. The validation can lead to `-EINVAL` being returned to the caller early if the params don't match the spec, but it doesn't leak any kernel data.

The `kapi_validate_syscall_params` function itself checks the number of parameters and their validity against the specification, and it does so with checks for expected counts and types against the spec. If the parameters are valid, the normal syscall handler runs. There’s also the `kapi_trace_format_params` function, but it constructs a string to be passed to tracepoints, and it is a null-terminated string. It never reads or exposes uninitialized memory.

The code changes are mostly adding kerneldoc comments, such as in `fs/open.c`, `fs/read_write.c`, and `mm/madvise.c`, for the various syscalls. The build process uses `kernel-doc -apispec` to generate `.apispec.h` files which contain the `DEFINE_KERNEL_API_SPEC` macro calls to populate the static `kernel_api_spec` data. The linker script is modified to create the `.kapi_specs` section where these structs reside.

Now, critically, the code explicitly initializes all structures and data used for this validation. The static `kernel_api_spec` structs are in `.rodata`, thus zero-initialized. Dynamic allocations are explicitly zeroed. Nothing is sent directly to userspace except via debugfs/tracepoints, and these output formatted strings using `scnprintf`, or JSON in the case of debugfs, which avoids uninitialized data leaks. The tracepoints use `memset` on buffers before writing to them.

Considering the prompt's rules, this means that while the patch introduces checks of syscall parameters, it rejects invalid values early, preventing the syscall handler from executing. This doesn't involve conditional logic depending on uninitialized data, nor does it involve the allocation of complex data structures with uninitialized fields. There are bounds checks on data that's being formatted or read, and if the data is invalid the call fails. Any bugs that might exist here would primarily be buffer overflows within format string construction, or potential race conditions or deadlocks involving the `api_spec_mutex` or potentially an out-of-bounds read due to a linker script section. These types of bugs are fully addressable by existing KASAN/LOCKDEP/etc. detectors. There's nothing here that introduces a new or hidden KMSAN-specific uninitialized memory risk.

Therefore, `NeedsKMSAN=false`.



28/3 2026/10/08 12:38 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch series introduces the Kernel API Specification (KAPI) framework, which provides machine-readable API specifications and optional runtime validation of syscall parameters (via SYSCALL_DEFINEx wrappers), debugfs introspection, tracepoints, and userspace tooling.\n\nEvaluating KMSAN vs. KASAN applicability:\n- KMSAN detects reads of uninitialized memory and info-leaks to userspace. This patch does not introduce any uninitialized memory risks:\n  1. All API specifications compiled into the kernel are static const objects placed in .rodata, where uninitialized fields are guaranteed to be zero-initialized by the C compiler.\n  2. Dynamic registrations use kzalloc_obj, and KUnit test specs use kunit_kzalloc / memset(..., 0, ...).\n  3. No kernel structures or uninitialized paddings are copied to userspace via copy_to_user, put_user, netlink, ioctl, or socket options. The debugfs interface outputs text and JSON formatted via seq_file (scnprintf), and the tracepoint helper explicitly memsets __entry-\u003eargs.\n  4. The runtime validation hooks only perform sanity checks on syscall arguments (checking ranges, masks, and user pointer accessibility via access_ok/strnlen_user) and early-return -EINVAL on violation, without altering buffer lengths or leaving kernel data uninitialized.\n- Potential risks in this patch (e.g. out-of-bounds reads during .kapi_specs section traversal, format string truncation/buffer bounds, or deadlocks on api_spec_mutex) are already detected by standard tools like KASAN and LOCKDEP.\n\nTherefore, a dedicated KMSAN fuzzing session is not justified.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch series introduces the Kernel API Specification (KAPI) framework, which provides machine-readable API specifications and optional runtime validation of syscall parameters (via SYSCALL_DEFINEx wrappers), debugfs introspection, tracepoints, and userspace tooling.\n\nEvaluating KMSAN vs. KASAN applicability:\n- KMSAN detects reads of uninitialized memory and info-leaks to userspace. This patch does not introduce any uninitialized memory risks:\n  1. All API specifications compiled into the kernel are static const objects placed in .rodata, where uninitialized fields are guaranteed to be zero-initialized by the C compiler.\n  2. Dynamic registrations use kzalloc_obj, and KUnit test specs use kunit_kzalloc / memset(..., 0, ...).\n  3. No kernel structures or uninitialized paddings are copied to userspace via copy_to_user, put_user, netlink, ioctl, or socket options. The debugfs interface outputs text and JSON formatted via seq_file (scnprintf), and the tracepoint helper explicitly memsets __entry-\u003eargs.\n  4. The runtime validation hooks only perform sanity checks on syscall arguments (checking ranges, masks, and user pointer accessibility via access_ok/strnlen_user) and early-return -EINVAL on violation, without altering buffer lengths or leaving kernel data uninitialized.\n- Potential risks in this patch (e.g. out-of-bounds reads during .kapi_specs section traversal, format string truncation/buffer bounds, or deadlocks on api_spec_mutex) are already detected by standard tools like KASAN and LOCKDEP.\n\nTherefore, a dedicated KMSAN fuzzing session is not justified.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)