From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A8078381AE2; Fri, 29 May 2026 23:33:18 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097605; cv=none; b=W66RJjVdt0qgN2Gygjc16rT3osrhK8cWx+WfcI/p/G0/IX7RIfcLWnoe/sM1HnuhG5sGW1gwamRtvXehrb6rwT+uYZxkWjK8/Qn0f52fkPve0pm1DJkhMs2yHeQ2NfrIByh5H8jq9htKJGwMbqvBoKWuumOr2JcfL8QMGlxDgm4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097605; c=relaxed/simple; bh=rQm1jz4Onc/S3hhGAE43eQ0SPKZLQFOTySWbBcoS9V0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=pv5sHOmt28jRgDYpDkFAuzAqRNS+oNQVzn6Dm55A635sie2Ni6bsL2fixf9suLjEH3EO51Usj5FJrD+LrSdqSUkxX6/BbLCF9Bh7fnzD35dl9DoXpSbvZJBc4QrKJiAb6hiSZXvthCVfF3mBUusCthqJ/Fvo/DeMqN4as9YyhPg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=UKaBVuk0; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="UKaBVuk0" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 60C631F00899; Fri, 29 May 2026 23:33:15 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097598; bh=0oyGBVcgJixD3wZKOnVAaqzUO79nzqlDb4tWu753PNc=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=UKaBVuk0z4kbFyYfd4f9P8+a8opAlc0QYkVpqZl42QozKTXNSUizjlBg9ciGzZOFH 1+//iQIJNaBLtDhx5gsfMNpuB9rbQNYOWDxD4djQhL/jTtKAMoEAIneQUiEipsivLt 9KBtKg1iJ+ZjL0JCV4xPTQbW0VHYkLkRNnmDdWw/2O0iL5rX/C78zOWQ8r9yL/QnAy rjRpOwPLn6cTuPFXxZnQ/6QQ7wDMuNOel9XafSN3D2cIpSqDoUy1CNjYhKjCtZa9Dd qtwsoxemAcMQc2pQnGzbsIbRWa+eQX/4Dq8/xlHZs2V4lkMhfBZ2j3fAZk1SKMtbPO W/6VkzSbpoA5A== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 01/11] kernel/api: introduce kernel API specification framework Date: Fri, 29 May 2026 19:33:00 -0400 Message-ID: <20260529233311.1901670-2-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add a framework for formally documenting kernel APIs with inline specifications. This framework provides: - Structured API documentation with parameter specifications, return values, error conditions, and execution context requirements - Runtime validation capabilities for debugging (CONFIG_KAPI_RUNTIME_CHECKS) - Support for both internal kernel APIs and system calls The framework stores specifications in a dedicated ELF section and provides infrastructure for: - Compile-time validation of specifications - Runtime querying of API documentation - Integration with existing SYSCALL_DEFINE macros This commit introduces the core infrastructure without modifying any existing APIs. Subsequent patches will add specifications to individual subsystems. Signed-off-by: Sasha Levin --- .gitignore | 1 + Documentation/dev-tools/index.rst | 1 + Documentation/dev-tools/kernel-api-spec.rst | 649 +++++++++ MAINTAINERS | 10 + arch/x86/include/asm/syscall_wrapper.h | 40 + include/asm-generic/vmlinux.lds.h | 28 + include/linux/kernel_api_spec.h | 1270 +++++++++++++++++ include/linux/syscalls.h | 38 + init/Kconfig | 2 + kernel/Makefile | 1 + kernel/api/Kconfig | 74 + kernel/api/Makefile | 14 + kernel/api/internal.h | 21 + kernel/api/kapi_kunit.c | 538 ++++++++ kernel/api/kernel_api_spec.c | 1363 +++++++++++++++++++ 15 files changed, 4050 insertions(+) create mode 100644 Documentation/dev-tools/kernel-api-spec.rst create mode 100644 include/linux/kernel_api_spec.h create mode 100644 kernel/api/Kconfig create mode 100644 kernel/api/Makefile create mode 100644 kernel/api/internal.h create mode 100644 kernel/api/kapi_kunit.c create mode 100644 kernel/api/kernel_api_spec.c diff --git a/.gitignore b/.gitignore index 3a7241c941f5e..7130001e444f1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # .* *.a +*.apispec.h *.asn1.[ch] *.bin *.bz2 diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/in= dex.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/de= v-tools/kernel-api-spec.rst new file mode 100644 index 0000000000000..395c2294d5209 --- /dev/null +++ b/Documentation/dev-tools/kernel-api-spec.rst @@ -0,0 +1,649 @@ +.. SPDX-License-Identifier: GPL-2.0 + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +Kernel API Specification Framework +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +:Author: Sasha Levin + +.. contents:: Table of Contents + :depth: 3 + :local: + +Introduction +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +The Kernel API Specification Framework (KAPI) provides a comprehensive sys= tem for +formally documenting, validating, and introspecting kernel APIs. This fram= ework +addresses the long-standing challenge of maintaining accurate, machine-rea= dable +documentation for the thousands of internal kernel APIs and system calls. + +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 p= rocess. + +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 form= ats for + use by static analyzers, documentation generators, and development tool= s. + +4. **Formalize Contracts**: Explicitly document API contracts including pa= rameter + constraints, execution contexts, locking requirements, and side effects. + +Architecture Overview +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 + =E2=94=9C=E2=94=80=E2=94=80 Basic Information + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 name (API function name) + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 version (specification version) + =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 description (human-readable de= scription) + =E2=94=82 + =E2=94=9C=E2=94=80=E2=94=80 Parameters (up to 16) + =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 kapi_param_spec + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 name + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 type (int, pointer, string= , etc.) + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 direction (in, out, inout) + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 constraints (range, mask, = enum values) + =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 validation rules + =E2=94=82 + =E2=94=9C=E2=94=80=E2=94=80 Return Value + =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 kapi_return_spec + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 type + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 success conditions + =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 validation rules + =E2=94=82 + =E2=94=9C=E2=94=80=E2=94=80 Error Conditions (up to 32) + =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 kapi_error_spec + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 error code + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 condition description + =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 recovery advice + =E2=94=82 + =E2=94=9C=E2=94=80=E2=94=80 Execution Context + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 allowed contexts (process, int= errupt, etc.) + =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 locking requirements + =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 preemption/interrupt state + =E2=94=82 + =E2=94=94=E2=94=80=E2=94=80 Side Effects + =E2=94=9C=E2=94=80=E2=94=80 memory allocation + =E2=94=9C=E2=94=80=E2=94=80 state changes + =E2=94=94=E2=94=80=E2=94=80 signal handling + +Usage Guide +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Basic API Specification +----------------------- + +API specifications are written as KAPI-annotated kerneldoc comments direct= ly in +the source file, immediately preceding the function implementation. The ``= kapi`` +tool extracts these annotations to produce structured specifications. + +.. code-block:: c + + /** + * kmalloc - allocate kernel memory + * @size: Number of bytes to allocate + * @flags: Allocation flags (GFP_*) + * + * contexts: process, softirq, hardirq + * + * param: size + * type: uint, input + * constraint-type: range(0, KMALLOC_MAX_SIZE) + * + * param: flags + * type: uint, input + * constraint-type: mask(0x1ffffff) + * + * error: ENOMEM, Out of memory + * desc: Insufficient memory available for the requested allocation. + * + * side-effect: alloc_memory + * target: kernel heap + * desc: Allocates memory from kernel heap + */ + void *kmalloc(size_t size, gfp_t flags) + { + /* Implementation */ + } + +DSL reference: + +* ``contexts:`` =E2=80=94 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:`` =E2=80=94 parameter type plus direction/qualifier flags on a s= ingle + 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:`` =E2=80=94 a ``KAPI_CONSTRAINT_*`` enum token or a + function-call expression. ``range(lo, hi)``, ``mask(expr)``, + ``enum(v1, v2, =E2=80=A6)``, ``buffer(size_param_idx)``, ``alignment(N)`= `, + ``user_string(N)``, ``user_path``, ``user_ptr``, ``power_of_two``, + ``page_aligned``, ``nonzero``, ``custom(fn_name)``. The function-call + form populates the matching aux fields + (``range:`` / ``valid-mask:`` / ``size-param:``). +* ``lock: =E2=80=A6 type:`` accepts ``mutex``, ``spinlock``, ``rwlock``, + ``seqlock``, ``rcu``, ``semaphore``, ``custom`` or ``KAPI_LOCK_*``. +* ``signal: =E2=80=A6 direction:`` accepts ``receive``, ``send``, ``handle= ``, + ``block``, ``ignore`` (bitmask, joinable with ``|`` or ``,``). +* ``signal: =E2=80=A6 action:`` accepts ``default``, ``terminate``, ``core= dump``, + ``stop``, ``continue``, ``custom``, ``return``, ``restart``, + ``queue``, ``discard``, ``transform``. +* ``signal: =E2=80=A6 timing:`` accepts ``before``, ``during``, ``after``. +* ``capability: =E2=80=A6 type:`` accepts ``bypass_check``, ``increase_lim= it``, + ``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 ``|`` =E2=80=94 for example ``side-effect: resource_create |= alloc_memory``. +* ``return: =E2=80=A6 type:`` reuses the ``type:`` aliases above. +* ``return: =E2=80=A6 check-type:`` accepts ``exact``, ``range``, + ``error_check``, ``fd``, ``custom``, ``no_return``. The value + subfields (``success:``, ``success-range:``, ``error-values:``) + populate the corresponding fields in ``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 + ``errno:``, ``priority:``, ``restartable:``, ``interruptible:``, + and ``queue:`` subfields. +* ``state-trans:`` takes ``from:``, ``to:``, ``object:``, + optional ``condition:``, and ``desc:`` subfields. +* ``struct-field:`` takes a ``NAME, TYPE, description`` header + for struct-member specs; ``struct-field-range:`` and + ``struct-field-mask:`` attach numeric constraints to a + previously-declared field. +* ``long-desc:`` is a free-form multi-line prose block that + populates ``long_description`` in the spec. +* ``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 specific= ation +and validate parameters before and after the syscall executes. + +IOCTL Specification +------------------- + +.. note:: + + IOCTL specifications are planned for a follow-up series. The + framework's ``struct-field:`` / ``struct-field-range:`` annotations + and the ``KAPI_STRUCT_FIELD(...)`` macro family are already in place, + but no in-tree ioctl spec ships yet. A worked example will land + alongside the first real ioctl migration. + +Runtime Validation +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Enabling Validation +------------------- + +Runtime validation is controlled by kernel configuration: + +1. Enable ``CONFIG_KAPI_SPEC`` to build the framework +2. Enable ``CONFIG_KAPI_RUNTIME_CHECKS`` for runtime validation + +Validation Behavior +------------------- + +When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, all registered API specifi= cations +are validated automatically at call time. The framework checks parameter c= onstraints, +execution context, and return values. Parameter violations are reported via +``pr_warn_ratelimited`` and return value violations via ``WARN_ONCE`` to a= void +flooding the kernel log. + +Custom Validators +----------------- + +Parameters can use the ``KAPI_CONSTRAINT_CUSTOM`` constraint type to regis= ter +custom validation functions via the ``validate`` field in the constraint s= pec: + +.. code-block:: c + + static bool validate_buffer_size(s64 value) + { + size_t size =3D (size_t)value; + + return size > 0 && size <=3D MAX_BUFFER_SIZE; + } + + /* In the constraint definition: */ + .type =3D KAPI_CONSTRAINT_CUSTOM, + .validate =3D validate_buffer_size, + +Performance Considerations +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D + +Memory Overhead +--------------- + +Each compiled spec is roughly 26 KB (``readelf -sW vmlinux | grep +__kapi_spec_``), dominated by the fixed-bound arrays +``errors[32]``, ``signals[32]``, ``constraints[32]``, +``side_effects[32]``, ``locks[16]``, and ``params[16]``. With +today's four syscall specs, ``.kapi_specs`` and the backing +``.data`` objects occupy ~105 KB. Converting the fixed-bound +arrays to pointer-plus-count pairs would cut the footprint +further and is planned as a follow-up series. Strategies for +production kernels: + +1. Build with ``CONFIG_KAPI_SPEC=3Dn`` =E2=80=94 no code or data from the + framework is emitted. +2. Enable specs selectively per subsystem as they land upstream. + +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). +Expect tens to hundreds of nanoseconds of extra work per call +depending on parameter count and constraint complexity; profile +before enabling on workloads where syscall latency matters. +``CONFIG_KAPI_RUNTIME_CHECKS=3Dn`` compiles the validators away +entirely. + +Optimization Strategies +----------------------- + +1. **Compile-time optimization**: When validation is disabled, all + validation code is optimized away by the compiler. + +2. **Selective enablement**: Enable ``CONFIG_KAPI_RUNTIME_CHECKS`` + only in development/testing kernels, not in production. + +Documentation Generation +------------------------ + +The framework exports specifications via debugfs that can be used +to generate documentation. The ``kapi`` tool provides comprehensive +extraction and formatting capabilities for kernel API specifications. + +The kapi Tool +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Overview +-------- + +The ``kapi`` tool is a userspace utility that extracts and displays kernel= API +specifications from multiple sources. It provides a unified interface to a= ccess +API documentation whether from compiled kernels, source code, or runtime s= ystems. + +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 ``: Extract from compiled kernel binary +- ``--source ``: Extract from kernel source code +- ``--debugfs ``: Extract from debugfs (default: /sys/kernel/debug) +- ``-f, --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 kernel/sched/core.c + + # Get details for specific API + $ kapi --source /path/to/linux sys_sched_yield + +**2. Vmlinux Mode** + +Extract from compiled kernel with debug symbols:: + + # List all APIs in vmlinux + $ kapi --vmlinux /boot/vmlinux-5.15.0 + + # 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 +-------------- + +**Plain Text Format** (default):: + + $ kapi sys_read + + Detailed information for sys_read: + =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + Description: Read from a file descriptor + + Detailed Description: + Reads up to count bytes from file descriptor fd into the buffer starti= ng at buf. + + Execution Context: + - KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE + + Parameters (3): + +**JSON Format**:: + + $ kapi --format json sys_read + { + "api_details": { + "name": "sys_read", + "description": "Read from a file descriptor", + "long_description": "Reads up to count bytes...", + "context_flags": ["KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE"] + } + } + +**ReStructuredText Format**:: + + $ kapi --format rst sys_read + + sys_read + =3D=3D=3D=3D=3D=3D=3D=3D + + **Read from a file descriptor** + + Reads up to count bytes from file descriptor fd into the buffer... + +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 drivers/gpu/drm/ + +**Integration with other tools**:: + + # Find all APIs that can sleep + $ kapi --format json | jq '.apis[] | select(.context_flags[] | contain= s("SLEEPABLE"))' + + # Generate markdown documentation + $ kapi --format rst sys_mmap | pandoc -f rst -t markdown + +**Debugging and analysis**:: + + # Compare API between kernel versions + $ diff <(kapi --vmlinux vmlinux-5.10) <(kapi --vmlinux vmlinux-5.15) + + # 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 specification macros using regular express= ions +2. **Vmlinux**: Reads the ``.kapi_specs`` ELF section from compiled kernels +3. **Debugfs**: Reads from ``/sys/kernel/debug/kapi/`` filesystem interface + +The tool supports all KAPI specification types: + +- System calls (kerneldoc annotations) +- Kernel functions (kerneldoc annotations with KAPI tags) + +IDE Integration +--------------- + +Modern IDEs can use the specification data for: + +- Parameter hints +- Type checking +- Context validation +- Error code documentation + +Testing Framework +----------------- + +The framework includes test helpers:: + + #ifdef CONFIG_KAPI_TESTING + /* Verify API behaves according to specification */ + kapi_test_api("kmalloc", test_cases); + #endif + +Best Practices +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Writing Specifications +---------------------- + +1. **Be Comprehensive**: Document all parameters, errors, and side effects +2. **Keep Updated**: Update specs when API behavior changes +3. **Use Examples**: Include usage examples in descriptions +4. **Validate Constraints**: Define realistic constraints for parameters +5. **Document Context**: Clearly specify allowed execution contexts + +Maintenance +----------- + +1. **Version Specifications**: Increment version when API changes +2. **Deprecation**: Mark deprecated APIs and suggest replacements +3. **Cross-reference**: Link related APIs in descriptions +4. **Test Specifications**: Verify specs match implementation + +Common Patterns +--------------- + +**Optional Parameters**: + +.. code-block:: c + + /** + * @optional_arg: Optional argument (may be NULL) + * + * param: optional_arg + * type: KAPI_TYPE_PTR + * flags: KAPI_PARAM_IN | KAPI_PARAM_OPTIONAL + */ + +**Buffer with Size Parameter**: + +.. code-block:: c + + /** + * @buf: User-space buffer + * + * param: buf + * type: KAPI_TYPE_USER_PTR + * flags: KAPI_PARAM_OUT | KAPI_PARAM_USER + * constraint-type: KAPI_CONSTRAINT_BUFFER + * size-param: 2 + */ + +**Callback Functions**: + +.. code-block:: c + + /** + * @callback: Callback function + * + * param: callback + * type: KAPI_TYPE_FUNC_PTR + * flags: KAPI_PARAM_IN + */ + +Troubleshooting +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Common Issues +------------- + +**Specification Not Found**:: + + kernel: KAPI: Specification for 'my_api' not found + + Solution: Ensure the KAPI-annotated kerneldoc comment is in the + same translation unit as the function implementation. + +**Validation Failures**:: + + kernel: KAPI: Validation failed for kmalloc parameter 'size': + value 5242880 exceeds maximum 4194304 + + Solution: Check parameter constraints or adjust specification if + the constraint is incorrect. + +**Build Errors**:: + + error: 'KAPI_TYPE_UNKNOWN' undeclared + + Solution: Include and ensure + CONFIG_KAPI_SPEC is enabled. + +Debug Options +------------- + +Enable verbose kernel logging to see KAPI validation messages:: + + echo 8 > /proc/sys/kernel/printk + +Future Directions +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Planned Features +---------------- + +1. **Automatic Extraction**: Tool to extract specifications from existing + kernel-doc comments + +2. **Contract Verification**: Static analysis to verify implementation + matches specification + +3. **Performance Profiling**: Measure actual API performance against + documented expectations + +4. **Fuzzing Integration**: Use specifications to guide intelligent + fuzzing of kernel APIs + +5. **Version Compatibility**: Track API changes across kernel versions + +Research Areas +-------------- + +1. **Formal Verification**: Use specifications for mathematical proofs + of correctness + +2. **Runtime Monitoring**: Detect specification violations in production + with minimal overhead + +3. **API Evolution**: Analyze how kernel APIs change over time + +4. **Security Applications**: Use specifications for security policy + enforcement + +Contributing +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Submitting Specifications +------------------------- + +1. Add specifications to the same file as the API implementation +2. Follow existing patterns and naming conventions +3. Test with CONFIG_KAPI_RUNTIME_CHECKS enabled +4. Run scripts/checkpatch.pl on your changes + +Review Criteria +--------------- + +Specifications will be reviewed for: + +1. **Completeness**: All parameters and errors documented +2. **Accuracy**: Specification matches implementation +3. **Clarity**: Descriptions are clear and helpful +4. **Consistency**: Follows framework conventions +5. **Performance**: No unnecessary runtime overhead + +Contact +------- + +- Maintainer: Sasha Levin diff --git a/MAINTAINERS b/MAINTAINERS index d1cc0e12fe1f0..0d14205077908 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13817,6 +13817,16 @@ W: https://linuxtv.org T: git git://linuxtv.org/media.git F: drivers/media/radio/radio-keene* =20 +KERNEL API SPECIFICATION FRAMEWORK (KAPI) +M: Sasha Levin +L: linux-api@vger.kernel.org +S: Maintained +F: Documentation/dev-tools/kernel-api-spec.rst +F: include/linux/kernel_api_spec.h +F: kernel/api/ +F: tools/kapi/ +F: tools/lib/python/kdoc/kdoc_apispec.py + KERNEL AUTOMOUNTER M: Ian Kent L: autofs@vger.kernel.org diff --git a/arch/x86/include/asm/syscall_wrapper.h b/arch/x86/include/asm/= syscall_wrapper.h index 7e88705e907f4..2cd960ed80fd1 100644 --- a/arch/x86/include/asm/syscall_wrapper.h +++ b/arch/x86/include/asm/syscall_wrapper.h @@ -7,6 +7,14 @@ #define _ASM_X86_SYSCALL_WRAPPER_H =20 #include +#ifdef CONFIG_KAPI_RUNTIME_CHECKS +struct kernel_api_spec; +const struct kernel_api_spec *kapi_get_spec(const char *name); +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); +#endif =20 extern long __x64_sys_ni_syscall(const struct pt_regs *regs); extern long __ia32_sys_ni_syscall(const struct pt_regs *regs); @@ -220,6 +228,37 @@ extern long __ia32_sys_ni_syscall(const struct pt_regs= *regs); =20 #endif /* CONFIG_COMPAT */ =20 +#ifdef CONFIG_KAPI_RUNTIME_CHECKS +#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__));\ + static inline long __do_kapi_sys##name(__MAP(x,__SC_DECL,__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 =3D __do_kapi_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; \ + } \ + static inline long __do_kapi_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))\ + { \ + const struct kernel_api_spec *__spec =3D kapi_get_spec("sys" #name); \ + if (__spec) { \ + s64 __params[x] =3D { __MAP(x,__SC_CAST_TO_S64,__VA_ARGS__) }; \ + int __ret =3D kapi_validate_syscall_params(__spec, __params, x); \ + if (__ret) \ + return __ret; \ + } \ + long ret =3D __do_sys##name(__MAP(x,__SC_ARGS,__VA_ARGS__)); \ + if (__spec) { \ + kapi_validate_syscall_return(__spec, (s64)ret); \ + } \ + return ret; \ + } \ + static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) +#else /* !CONFIG_KAPI_RUNTIME_CHECKS */ #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__));\ @@ -233,6 +272,7 @@ extern long __ia32_sys_ni_syscall(const struct pt_regs = *regs); return ret; \ } \ static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) +#endif /* CONFIG_KAPI_RUNTIME_CHECKS */ =20 /* * As the generic SYSCALL_DEFINE0() macro does not decode any parameters f= or diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinu= x.lds.h index 1e1580febe4b9..60f2bb05b7bf3 100644 --- a/include/asm-generic/vmlinux.lds.h +++ b/include/asm-generic/vmlinux.lds.h @@ -296,6 +296,33 @@ #define TRACE_SYSCALLS() #endif =20 +#ifdef CONFIG_KAPI_SPEC +/* + * KAPI_SPECS - Include kernel API specifications in current section + * + * The .kapi_specs input section has 32-byte alignment requirement from + * the compiler, so we must align to 32 bytes before setting the start + * symbol to avoid padding between the symbol and actual data. + */ +#define KAPI_SPECS() \ + . =3D ALIGN(32); \ + __start_kapi_specs =3D .; \ + KEEP(*(.kapi_specs)) \ + __stop_kapi_specs =3D .; + +/* For placing KAPI specs in a dedicated section */ +#define KAPI_SPECS_SECTION() \ + .kapi_specs : AT(ADDR(.kapi_specs) - LOAD_OFFSET) { \ + . =3D ALIGN(32); \ + __start_kapi_specs =3D .; \ + KEEP(*(.kapi_specs)) \ + __stop_kapi_specs =3D .; \ + } +#else +#define KAPI_SPECS() +#define KAPI_SPECS_SECTION() +#endif + #ifdef CONFIG_BPF_EVENTS #define BPF_RAW_TP() STRUCT_ALIGN(); \ BOUNDED_SECTION_BY(__bpf_raw_tp_map, __bpf_raw_tp) @@ -485,6 +512,7 @@ . =3D 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/kernel_api_spec.h b/include/linux/kernel_api_spe= c.h new file mode 100644 index 0000000000000..4d0ddc8d1ac88 --- /dev/null +++ b/include/linux/kernel_api_spec.h @@ -0,0 +1,1270 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2026 Sasha Levin + * + * kernel_api_spec.h - Kernel API Formal Specification Framework + * + * This framework provides structures and macros to formally specify kerne= l APIs + * in both human and machine-readable formats. It supports comprehensive d= ocumentation + * of function signatures, parameters, return values, error conditions, an= d constraints. + */ + +#ifndef _LINUX_KERNEL_API_SPEC_H +#define _LINUX_KERNEL_API_SPEC_H + +#include +#include +#include +#include +#include +#include +#include + +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 - validated in process context + * @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 =3D 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 - validated in process context */ + 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 =3D (1 << 0), + KAPI_PARAM_OUT =3D (1 << 1), + KAPI_PARAM_INOUT =3D (KAPI_PARAM_IN | KAPI_PARAM_OUT), + KAPI_PARAM_OPTIONAL =3D (1 << 3), + KAPI_PARAM_CONST =3D (1 << 4), + KAPI_PARAM_VOLATILE =3D (1 << 5), + KAPI_PARAM_USER =3D (1 << 6), + KAPI_PARAM_DMA =3D (1 << 7), + KAPI_PARAM_ALIGNED =3D (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 =3D (1 << 0), + KAPI_CTX_SOFTIRQ =3D (1 << 1), + KAPI_CTX_HARDIRQ =3D (1 << 2), + KAPI_CTX_NMI =3D (1 << 3), + KAPI_CTX_ATOMIC =3D (1 << 4), + KAPI_CTX_SLEEPABLE =3D (1 << 5), + KAPI_CTX_PREEMPT_DISABLED =3D (1 << 6), + KAPI_CTX_IRQ_DISABLED =3D (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 =3D 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 sp= ecified 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 len= gth range + * @KAPI_CONSTRAINT_USER_PATH: Userspace pathname string (validated for ac= cessibility and PATH_MAX) + * @KAPI_CONSTRAINT_USER_PTR: Userspace pointer (validated for accessibili= ty 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 =3D 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 (>=3D 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 =3D 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 =3D (1 << 0), + KAPI_SIGNAL_SEND =3D (1 << 1), + KAPI_SIGNAL_HANDLE =3D (1 << 2), + KAPI_SIGNAL_BLOCK =3D (1 << 3), + KAPI_SIGNAL_IGNORE =3D (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 =3D 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", "coalesc= e") + * @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 d= enied + * @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 =3D 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 =3D 0, + KAPI_EFFECT_ALLOC_MEMORY =3D (1 << 0), + KAPI_EFFECT_FREE_MEMORY =3D (1 << 1), + KAPI_EFFECT_MODIFY_STATE =3D (1 << 2), + KAPI_EFFECT_SIGNAL_SEND =3D (1 << 3), + KAPI_EFFECT_FILE_POSITION =3D (1 << 4), + KAPI_EFFECT_LOCK_ACQUIRE =3D (1 << 5), + KAPI_EFFECT_LOCK_RELEASE =3D (1 << 6), + KAPI_EFFECT_RESOURCE_CREATE =3D (1 << 7), + KAPI_EFFECT_RESOURCE_DESTROY =3D (1 << 8), + KAPI_EFFECT_SCHEDULE =3D (1 << 9), + KAPI_EFFECT_HARDWARE =3D (1 << 10), + KAPI_EFFECT_NETWORK =3D (1 << 11), + KAPI_EFFECT_FILESYSTEM =3D (1 << 12), + KAPI_EFFECT_PROCESS_STATE =3D (1 << 13), + KAPI_EFFECT_IRREVERSIBLE =3D (1 << 14), +}; + +/** + * struct kapi_side_effect - Side effect specification + * @type: Bitmask of effect types + * @target: What is affected (e.g., "process memory", "file descriptor tab= le") + * @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 =3D 'KAP1' */ + u32 param_count; + struct kapi_param_spec params[KAPI_MAX_PARAMS]; + + /* Return value */ + u32 return_magic; /* 0x4B415232 =3D 'KAR2' */ + struct kapi_return_spec return_spec; + + /* Errors */ + u32 error_magic; /* 0x4B414533 =3D 'KAE3' */ + u32 error_count; + struct kapi_error_spec errors[KAPI_MAX_ERRORS]; + + /* Locking */ + u32 lock_magic; /* 0x4B414C34 =3D 'KAL4' */ + u32 lock_count; + struct kapi_lock_spec locks[KAPI_MAX_LOCKS]; + + /* Constraints */ + u32 constraint_magic; /* 0x4B414335 =3D 'KAC5' */ + u32 constraint_count; + struct kapi_constraint_spec constraints[KAPI_MAX_CONSTRAINTS]; + + /* Additional information */ + u32 info_magic; /* 0x4B414936 =3D 'KAI6' */ + const char *examples; + const char *notes; + + /* Signal specifications */ + u32 signal_magic; /* 0x4B415337 =3D 'KAS7' */ + u32 signal_count; + struct kapi_signal_spec signals[KAPI_MAX_SIGNALS]; + + /* Signal mask specifications */ + u32 sigmask_magic; /* 0x4B414D38 =3D 'KAM8' */ + u32 signal_mask_count; + struct kapi_signal_mask_spec signal_masks[KAPI_MAX_SIGNALS]; + + /* Structure specifications */ + u32 struct_magic; /* 0x4B415439 =3D 'KAT9' */ + u32 struct_spec_count; + struct kapi_struct_spec struct_specs[KAPI_MAX_STRUCT_SPECS]; + + /* Side effects */ + u32 effect_magic; /* 0x4B414641 =3D 'KAFA' */ + u32 side_effect_count; + struct kapi_side_effect side_effects[KAPI_MAX_SIDE_EFFECTS]; + + /* State transitions */ + u32 trans_magic; /* 0x4B415442 =3D 'KATB' */ + u32 state_trans_count; + struct kapi_state_transition state_transitions[KAPI_MAX_STATE_TRANS]; + + /* Capability specifications */ + u32 cap_magic; /* 0x4B414343 =3D '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 =3D __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") =3D \ + &__kapi_spec_##func_name; \ + const struct kernel_api_spec __kapi_spec_##func_name =3D { \ + .name =3D __stringify(func_name), \ + .version =3D 1, + +/** + * KAPI_DESCRIPTION - Set API description + * @desc: Description string + */ +#define KAPI_DESCRIPTION(desc) \ + .description =3D desc, + +/** + * KAPI_LONG_DESC - Set detailed API description + * @desc: Detailed description string + */ +#define KAPI_LONG_DESC(desc) \ + .long_description =3D desc, + +/** + * KAPI_CONTEXT - Set execution context flags + * @flags: Context flags (OR'ed KAPI_CTX_* values) + */ +#define KAPI_CONTEXT(flags) \ + .context_flags =3D 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] =3D { \ + .name =3D pname, \ + .type_name =3D ptype, \ + .description =3D pdesc, + +#define KAPI_PARAM_TYPE(ptype) \ + .type =3D ptype, + +#define KAPI_PARAM_FLAGS(pflags) \ + .flags =3D pflags, + +#define KAPI_PARAM_SIZE(psize) \ + .size =3D psize, + +#define KAPI_PARAM_RANGE(pmin, pmax) \ + .min_value =3D pmin, \ + .max_value =3D pmax, + +#define KAPI_PARAM_CONSTRAINT_TYPE(ctype) \ + .constraint_type =3D ctype, + +#define KAPI_PARAM_CONSTRAINT(desc) \ + .constraints =3D desc, + +#define KAPI_PARAM_VALID_MASK(mask) \ + .valid_mask =3D mask, + +#define KAPI_PARAM_ENUM_VALUES(values) \ + .enum_values =3D (values), \ + .enum_count =3D sizeof(values) / sizeof((values)[0]), + +#define KAPI_PARAM_ALIGNMENT(align) \ + .alignment =3D 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 =3D (idx) + 1, + +/** + * KAPI_PARAM_COUNT - Set the number of parameters + * @n: Number of parameters + */ +#define KAPI_PARAM_COUNT(n) \ + .param_magic =3D KAPI_MAGIC_PARAMS, \ + .param_count =3D n, + +/** + * KAPI_RETURN - Define return value specification + * @rtype: Return type name + * @rdesc: Return value description + */ +#define KAPI_RETURN(rtype, rdesc) \ + .return_magic =3D KAPI_MAGIC_RETURN, \ + .return_spec =3D { \ + .type_name =3D rtype, \ + .description =3D rdesc, + +#define KAPI_RETURN_SUCCESS(val, ...) \ + .success_value =3D val, + +#define KAPI_RETURN_TYPE(rtype) \ + .type =3D rtype, + +#define KAPI_RETURN_CHECK_TYPE(ctype) \ + .check_type =3D ctype, + +#define KAPI_RETURN_ERROR_VALUES(values) \ + .error_values =3D values, + +#define KAPI_RETURN_ERROR_COUNT(count) \ + .error_count =3D count, + +#define KAPI_RETURN_SUCCESS_RANGE(min, max) \ + .success_min =3D min, \ + .success_max =3D 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] =3D { \ + .error_code =3D ecode, \ + .name =3D ename, \ + .condition =3D econd, \ + .description =3D edesc, \ + }, + +/** + * KAPI_ERROR_COUNT - Set the number of errors + * @n: Number of errors + */ +#define KAPI_ERROR_COUNT(n) \ + .error_magic =3D KAPI_MAGIC_ERRORS, \ + .error_count =3D n, + +/** + * KAPI_LOCK - Define a lock requirement + * @idx: Lock index + * @lname: Lock name + * @ltype: Lock type + */ +#define KAPI_LOCK(idx, lname, ltype) \ + .locks[idx] =3D { \ + .lock_name =3D lname, \ + .lock_type =3D ltype, + +#define KAPI_LOCK_ACQUIRED \ + .scope =3D KAPI_LOCK_ACQUIRES, + +#define KAPI_LOCK_RELEASED \ + .scope =3D KAPI_LOCK_RELEASES, + +#define KAPI_LOCK_HELD_ENTRY \ + .scope =3D KAPI_LOCK_CALLER_HELD, + +#define KAPI_LOCK_HELD_EXIT \ + .scope =3D KAPI_LOCK_CALLER_HELD, + +#define KAPI_LOCK_DESC(ldesc) \ + .description =3D ldesc, + +/** + * KAPI_CONSTRAINT - Define an additional constraint + * @idx: Constraint index + * @cname: Constraint name + * @cdesc: Constraint description + */ +#define KAPI_CONSTRAINT(idx, cname, cdesc) \ + .constraints[idx] =3D { \ + .name =3D cname, \ + .description =3D cdesc, + +#define KAPI_CONSTRAINT_EXPR(expr) \ + .expression =3D expr, + +/** + * KAPI_EXAMPLES - Set API usage examples + * @ex: Examples string + */ +#define KAPI_EXAMPLES(ex) \ + .info_magic =3D KAPI_MAGIC_INFO, \ + .examples =3D ex, + +/** + * KAPI_NOTES - Set API notes + * @n: Notes string + */ +#define KAPI_NOTES(n) \ + .notes =3D 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] =3D { \ + .signal_num =3D signum, \ + .signal_name =3D signame, \ + .direction =3D dir, \ + .action =3D act, + +#define KAPI_SIGNAL_TARGET(tgt) \ + .target =3D tgt, + +#define KAPI_SIGNAL_CONDITION(cond) \ + .condition =3D cond, + +#define KAPI_SIGNAL_DESC(desc) \ + .description =3D desc, + +#define KAPI_SIGNAL_RESTARTABLE \ + .restartable =3D true, + +#define KAPI_SIGNAL_SA_FLAGS_REQ(flags) \ + .sa_flags_required =3D flags, + +#define KAPI_SIGNAL_SA_FLAGS_FORBID(flags) \ + .sa_flags_forbidden =3D flags, + +#define KAPI_SIGNAL_ERROR(err) \ + .error_on_signal =3D err, + +#define KAPI_SIGNAL_TRANSFORM(sig) \ + .transform_to =3D sig, + +#define KAPI_SIGNAL_TIMING(when) \ + .timing =3D when, + +#define KAPI_SIGNAL_PRIORITY(prio) \ + .priority =3D prio, + +#define KAPI_SIGNAL_INTERRUPTIBLE \ + .interruptible =3D true, + +#define KAPI_SIGNAL_QUEUE(behavior) \ + .queue_behavior =3D behavior, + +#define KAPI_SIGNAL_STATE_REQ(state) \ + .state_required =3D state, + +#define KAPI_SIGNAL_STATE_FORBID(state) \ + .state_forbidden =3D state, + +#define KAPI_SIGNAL_COUNT(n) \ + .signal_magic =3D KAPI_MAGIC_SIGNALS, \ + .signal_count =3D 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] =3D { \ + .mask_name =3D name, \ + .description =3D 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 =3D { __VA_ARGS__ }, \ + .signal_count =3D sizeof((int[]){ __VA_ARGS__ }) / sizeof(int), + +/** + * 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] =3D { \ + .name =3D #sname, \ + .description =3D sdesc, + +#define KAPI_STRUCT_SIZE(ssize, salign) \ + .size =3D ssize, \ + .alignment =3D salign, + +#define KAPI_STRUCT_FIELD_COUNT(n) \ + .field_count =3D 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] =3D { \ + .name =3D fname, \ + .type =3D ftype, \ + .type_name =3D ftype_name, \ + .description =3D fdesc, + +#define KAPI_FIELD_OFFSET(foffset) \ + .offset =3D foffset, + +#define KAPI_FIELD_SIZE(fsize) \ + .size =3D fsize, + +#define KAPI_FIELD_FLAGS(fflags) \ + .flags =3D fflags, + +#define KAPI_FIELD_CONSTRAINT_RANGE(min, max) \ + .constraint_type =3D KAPI_CONSTRAINT_RANGE, \ + .min_value =3D min, \ + .max_value =3D max, + +#define KAPI_FIELD_CONSTRAINT_MASK(mask) \ + .constraint_type =3D KAPI_CONSTRAINT_MASK, \ + .valid_mask =3D mask, + +#define KAPI_FIELD_CONSTRAINT_ENUM(values) \ + .constraint_type =3D KAPI_CONSTRAINT_ENUM, \ + .enum_values =3D values, + +/* Counter for structure specifications */ +#define KAPI_STRUCT_SPEC_COUNT(n) \ + .struct_magic =3D KAPI_MAGIC_STRUCTS, \ + .struct_spec_count =3D n, + +/* Additional lock-related macros */ +#define KAPI_LOCK_COUNT(n) \ + .lock_magic =3D KAPI_MAGIC_LOCKS, \ + .lock_count =3D 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] =3D { \ + .type =3D etype, \ + .target =3D etarget, \ + .description =3D edesc, + +#define KAPI_EFFECT_CONDITION(cond) \ + .condition =3D cond, + +#define KAPI_EFFECT_REVERSIBLE \ + .reversible =3D 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] =3D { \ + .object =3D obj, \ + .from_state =3D from, \ + .to_state =3D to, \ + .description =3D desc, + +#define KAPI_STATE_TRANS_COND(cond) \ + .condition =3D cond, + +/* Counters for side effects and state transitions */ +#define KAPI_SIDE_EFFECT_COUNT(n) \ + .effect_magic =3D KAPI_MAGIC_EFFECTS, \ + .side_effect_count =3D n, + +#define KAPI_STATE_TRANS_COUNT(n) \ + .trans_magic =3D KAPI_MAGIC_TRANS, \ + .state_trans_count =3D n, + +/* Helper macros for common side effect patterns */ +#define KAPI_EFFECTS_MEMORY (KAPI_EFFECT_ALLOC_MEMORY | KAPI_EFFECT_FREE_M= EMORY) +#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) + +/* + * Helper macros for combining common parameter flag patterns. + * Note: KAPI_PARAM_IN, KAPI_PARAM_OUT, KAPI_PARAM_INOUT, and KAPI_PARAM_O= PTIONAL + * are already defined in enum kapi_param_flags - use those directly. + */ +#define KAPI_PARAM_FLAGS_INOUT (KAPI_PARAM_IN | KAPI_PARAM_OUT) +#define KAPI_PARAM_FLAGS_USER (KAPI_PARAM_USER | KAPI_PARAM_IN) + +/* 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] =3D { \ + .capability =3D cap, \ + .cap_name =3D name, \ + .action =3D act, + +#define KAPI_CAP_ALLOWS(desc) \ + .allows =3D desc, + +#define KAPI_CAP_WITHOUT(desc) \ + .without_cap =3D desc, + +#define KAPI_CAP_CONDITION(cond) \ + .check_condition =3D cond, + +#define KAPI_CAP_PRIORITY(prio) \ + .priority =3D prio, + +#define KAPI_CAP_ALTERNATIVE(caps, count) \ + .alternative =3D caps, \ + .alternative_count =3D count, + +/* Counter for capability specifications */ +#define KAPI_CAPABILITY_COUNT(n) \ + .cap_magic =3D KAPI_MAGIC_CAPS, \ + .capability_count =3D n, + +/* Validation and runtime checking */ + +#ifdef CONFIG_KAPI_RUNTIME_CHECKS +bool kapi_validate_param(const struct kapi_param_spec *param_spec, s64 val= ue); +bool kapi_validate_param_with_context(const struct kapi_param_spec *param_= spec, + s64 value, const s64 *all_params, int param_count); +int kapi_validate_syscall_param(const struct kernel_api_spec *spec, + int param_idx, s64 value); +int kapi_validate_syscall_params(const struct kernel_api_spec *spec, + const s64 *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 re= tval); +int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 r= etval); +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 int kapi_validate_syscall_param(const struct kernel_api_spec= *spec, + int param_idx, s64 value) +{ + return 0; +} +static inline int kapi_validate_syscall_params(const struct kernel_api_spe= c *spec, + const s64 *params, int param_count) +{ + return 0; +} +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 int kapi_validate_syscall_return(const struct kernel_api_spe= c *spec, s64 retval) +{ + return 0; +} +static inline void kapi_check_context(const struct kernel_api_spec *spec) = {} +#endif + +/* + * Export/query functions + * + * kapi_get_spec() returns a pointer that is valid only while the caller c= an + * guarantee the spec is not concurrently unregistered (e.g., module unloa= d). + * For static specs this is always safe; for dynamic specs callers must ho= ld + * a reference or ensure the owning module is pinned. + */ +const struct kernel_api_spec *kapi_get_spec(const char *name); +int kapi_export_json(const struct kernel_api_spec *spec, char *buf, size_t= size); +void kapi_print_spec(const struct kernel_api_spec *spec); + +/* Registration for dynamic APIs */ +int kapi_register_spec(const struct kernel_api_spec *spec); +void kapi_unregister_spec(const char *name); + +/* Helper to get parameter constraint info */ +static inline bool kapi_get_param_constraint(const char *api_name, int par= am_idx, + enum kapi_constraint_type *type, + u64 *valid_mask, s64 *min_val, s64 *max_val) +{ + const struct kernel_api_spec *spec; + + might_sleep(); + spec =3D kapi_get_spec(api_name); + + if (!spec || param_idx >=3D spec->param_count) + return false; + + if (type) + *type =3D spec->params[param_idx].constraint_type; + if (valid_mask) + *valid_mask =3D spec->params[param_idx].valid_mask; + if (min_val) + *min_val =3D spec->params[param_idx].min_value; + if (max_val) + *max_val =3D spec->params[param_idx].max_value; + + return true; +} + +#define KAPI_CONSTRAINT_COUNT(n) \ + .constraint_magic =3D KAPI_MAGIC_CONSTRAINTS, \ + .constraint_count =3D n, + +#endif /* _LINUX_KERNEL_API_SPEC_H */ diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h index 02bd6ddb62782..d0cf92503dcb1 100644 --- a/include/linux/syscalls.h +++ b/include/linux/syscalls.h @@ -133,6 +133,7 @@ struct file_attr; #define __SC_TYPE(t, a) t #define __SC_ARGS(t, a) a #define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof= (t) > sizeof(long)) +#define __SC_CAST_TO_S64(t, a) ((s64)(a)) =20 #ifdef CONFIG_FTRACE_SYSCALLS #define __SC_STR_ADECL(t, a) #a @@ -243,6 +244,42 @@ static inline int is_syscall_trace_event(struct trace_= event_call *tp_event) * done within __do_sys_*(). */ #ifndef __SYSCALL_DEFINEx +#ifdef CONFIG_KAPI_RUNTIME_CHECKS +#define __SYSCALL_DEFINEx(x, name, ...) \ + __diag_push(); \ + __diag_ignore(GCC, 8, "-Wattribute-alias", \ + "Type aliasing is used to sanitize syscall arguments");\ + asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \ + __attribute__((alias(__stringify(__se_sys##name)))); \ + ALLOW_ERROR_INJECTION(sys##name, ERRNO); \ + static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\ + static inline long __do_kapi_sys##name(__MAP(x,__SC_DECL,__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 =3D __do_kapi_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; \ + } \ + __diag_pop(); \ + static inline long __do_kapi_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))\ + { \ + const struct kernel_api_spec *__spec =3D kapi_get_spec("sys" #name); \ + if (__spec) { \ + s64 __params[x] =3D { __MAP(x,__SC_CAST_TO_S64,__VA_ARGS__) }; \ + int __ret =3D kapi_validate_syscall_params(__spec, __params, x); \ + if (__ret) \ + return __ret; \ + } \ + long ret =3D __do_sys##name(__MAP(x,__SC_ARGS,__VA_ARGS__)); \ + if (__spec) { \ + kapi_validate_syscall_return(__spec, (s64)ret); \ + } \ + return ret; \ + } \ + static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) +#else /* !CONFIG_KAPI_RUNTIME_CHECKS */ #define __SYSCALL_DEFINEx(x, name, ...) \ __diag_push(); \ __diag_ignore(GCC, 8, "-Wattribute-alias", \ @@ -261,6 +298,7 @@ static inline int is_syscall_trace_event(struct trace_e= vent_call *tp_event) } \ __diag_pop(); \ static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) +#endif /* CONFIG_KAPI_RUNTIME_CHECKS */ #endif /* __SYSCALL_DEFINEx */ =20 /* For split 64-bit arguments on 32-bit architectures */ diff --git a/init/Kconfig b/init/Kconfig index 7484cd703bc1a..15e458a0b70d8 100644 --- a/init/Kconfig +++ b/init/Kconfig @@ -2223,6 +2223,8 @@ source "kernel/Kconfig.kexec" =20 source "kernel/liveupdate/Kconfig" =20 +source "kernel/api/Kconfig" + endmenu # General setup =20 source "arch/Kconfig" diff --git a/kernel/Makefile b/kernel/Makefile index 6785982013dce..cedfe420d4716 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -59,6 +59,7 @@ obj-y +=3D dma/ obj-y +=3D entry/ obj-y +=3D unwind/ obj-$(CONFIG_MODULES) +=3D module/ +obj-$(CONFIG_KAPI_SPEC) +=3D api/ =20 obj-$(CONFIG_KCMP) +=3D kcmp.o obj-$(CONFIG_FREEZER) +=3D freezer.o diff --git a/kernel/api/Kconfig b/kernel/api/Kconfig new file mode 100644 index 0000000000000..5bc12e6a0fc7a --- /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 + help + Enable runtime validation of API usage against specifications. + This includes checking execution context requirements, parameter + validation, and lock state verification. + + 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. + + This adds overhead and should only be used for debugging and + development. The checks use WARN_ONCE to report violations. + + 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..abaad7bac4719 --- /dev/null +++ b/kernel/api/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for the Kernel API Specification Framework +# + +# Core API specification framework +obj-y +=3D kernel_api_spec.o + +# Debugfs interface for kernel API specs +obj-$(CONFIG_KAPI_SPEC_DEBUGFS) +=3D kapi_debugfs.o + +# KUnit tests +obj-$(CONFIG_KAPI_KUNIT_TEST) +=3D kapi_kunit.o + diff --git a/kernel/api/internal.h b/kernel/api/internal.h new file mode 100644 index 0000000000000..ec5851f127f87 --- /dev/null +++ b/kernel/api/internal.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2026 Sasha Levin + * + * 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 + +/* + * 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[]; + +#endif /* _KERNEL_API_INTERNAL_H */ diff --git a/kernel/api/kapi_kunit.c b/kernel/api/kapi_kunit.c new file mode 100644 index 0000000000000..747462b813c50 --- /dev/null +++ b/kernel/api/kapi_kunit.c @@ -0,0 +1,538 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026 Sasha Levin + * + * KUnit tests for the Kernel API Specification Framework + * + * Tests registration, lookup, validation, and JSON export functionality. + */ + +#include +#include +#include +#include +#include + +static void init_test_spec(struct kernel_api_spec *spec, const char *name) +{ + memset(spec, 0, sizeof(*spec)); + spec->name =3D name; + spec->version =3D 1; + spec->description =3D "Test API"; +} + +/* Test 1: kapi_register_spec with valid spec returns 0 */ +static void test_register_valid(struct kunit *test) +{ + struct kernel_api_spec *spec; + int ret; + + spec =3D kzalloc_obj(*spec, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + init_test_spec(spec, "test_register_valid"); + + ret =3D kapi_register_spec(spec); + KUNIT_EXPECT_EQ(test, ret, 0); + + kapi_unregister_spec("test_register_valid"); + kfree(spec); +} + +/* Test 2: 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 =3D kzalloc_obj(*spec, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + init_test_spec(spec, "test_lookup_func"); + + ret =3D kapi_register_spec(spec); + KUNIT_ASSERT_EQ(test, ret, 0); + + found =3D 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); +} + +/* Test 3: Double registration returns -EEXIST */ +static void test_double_register(struct kunit *test) +{ + struct kernel_api_spec *spec; + int ret; + + spec =3D kzalloc_obj(*spec, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + init_test_spec(spec, "test_double_reg"); + + ret =3D kapi_register_spec(spec); + KUNIT_ASSERT_EQ(test, ret, 0); + + ret =3D kapi_register_spec(spec); + KUNIT_EXPECT_EQ(test, ret, -EEXIST); + + kapi_unregister_spec("test_double_reg"); + kfree(spec); +} + +/* Test 4: 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 =3D kzalloc_obj(*spec, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + init_test_spec(spec, "test_unreg_func"); + + ret =3D kapi_register_spec(spec); + KUNIT_ASSERT_EQ(test, ret, 0); + + kapi_unregister_spec("test_unreg_func"); + + found =3D kapi_get_spec("test_unreg_func"); + KUNIT_EXPECT_NULL(test, found); + + kfree(spec); +} + +/* Test 5: kapi_get_spec(NULL) returns NULL */ +static void test_get_spec_null(struct kunit *test) +{ + const struct kernel_api_spec *found; + + found =3D kapi_get_spec(NULL); + KUNIT_EXPECT_NULL(test, found); +} + +/* Test 6: kapi_register_spec(NULL) returns -EINVAL */ +static void test_register_null(struct kunit *test) +{ + int ret; + + ret =3D kapi_register_spec(NULL); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); +} + +/* Test 7: Spec with empty name is rejected */ +static void test_register_empty_name(struct kunit *test) +{ + struct kernel_api_spec *spec; + int ret; + + spec =3D kzalloc_obj(*spec, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + /* spec->name =3D=3D NULL after zero-init; registration rejects it. */ + ret =3D kapi_register_spec(spec); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + + kfree(spec); +} + +#ifdef CONFIG_KAPI_RUNTIME_CHECKS + +/* Test 8: RANGE constraint - value in range is valid */ +static void test_constraint_range_valid(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_param"; + param.constraint_type =3D KAPI_CONSTRAINT_RANGE; + param.min_value =3D 0; + param.max_value =3D 100; + + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 50)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 100)); +} + +/* Test 9: RANGE constraint - value out of range is invalid */ +static void test_constraint_range_invalid(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_param"; + param.constraint_type =3D KAPI_CONSTRAINT_RANGE; + param.min_value =3D 0; + param.max_value =3D 100; + + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, -1)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 101)); +} + +/* Test 10: MASK constraint - valid bits pass */ +static void test_constraint_mask_valid(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_flags"; + param.constraint_type =3D KAPI_CONSTRAINT_MASK; + param.valid_mask =3D 0xFF; + + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0x00)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0x0F)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0xFF)); +} + +/* Test 11: MASK constraint - extra bits fail */ +static void test_constraint_mask_invalid(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_flags"; + param.constraint_type =3D KAPI_CONSTRAINT_MASK; + param.valid_mask =3D 0xFF; + + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0x100)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0x1FF)); +} + +/* Test 12: POWER_OF_TWO constraint */ +static void test_constraint_power_of_two(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_pot"; + param.constraint_type =3D KAPI_CONSTRAINT_POWER_OF_TWO; + + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 1)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 2)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 4)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 8)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 3)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 5)); +} + +/* Test 13: PAGE_ALIGNED constraint */ +static void test_constraint_page_aligned(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_page"; + param.constraint_type =3D KAPI_CONSTRAINT_PAGE_ALIGNED; + + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, PAGE_SIZE)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 2 * PAGE_SIZE)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 1)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, PAGE_SIZE - 1)); +} + +/* Test 14: NONZERO constraint */ +static void test_constraint_nonzero(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_nz"; + param.constraint_type =3D KAPI_CONSTRAINT_NONZERO; + + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 1)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, -1)); +} + +/* Test 15: Return value validation - success */ +static void test_return_validation(struct kunit *test) +{ + struct kernel_api_spec *spec; + + spec =3D kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + spec->name =3D "test_ret"; + spec->return_magic =3D KAPI_MAGIC_RETURN; + spec->return_spec.check_type =3D KAPI_RETURN_EXACT; + spec->return_spec.success_value =3D 0; + + KUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 0)); +} + +/* Test 16: Return value validation - known error */ +static void test_return_known_error(struct kunit *test) +{ + struct kernel_api_spec *spec; + + spec =3D kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + spec->name =3D "test_ret_err"; + spec->return_magic =3D KAPI_MAGIC_RETURN; + spec->return_spec.check_type =3D KAPI_RETURN_FD; + spec->error_count =3D 1; + spec->errors[0].error_code =3D -ENOENT; + spec->errors[0].name =3D "ENOENT"; + + /* -ENOENT is in the error list, so it's valid */ + KUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, -ENOENT)); +} + +/* Test 17: Return value validation - unknown error */ +static void test_return_unknown_error(struct kunit *test) +{ + struct kernel_api_spec *spec; + + spec =3D kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + spec->name =3D "test_ret_unk"; + spec->return_magic =3D KAPI_MAGIC_RETURN; + spec->return_spec.check_type =3D KAPI_RETURN_FD; + spec->error_count =3D 1; + spec->errors[0].error_code =3D -ENOENT; + spec->errors[0].name =3D "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)); +} + +/* Test 18: ALIGNMENT constraint */ +static void test_constraint_alignment(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_align"; + param.constraint_type =3D KAPI_CONSTRAINT_ALIGNMENT; + param.alignment =3D 8; + + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 8)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 16)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 1)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 7)); +} + +/* Test 19: FD validation rejects values > INT_MAX */ +static void test_fd_int_overflow(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_fd"; + param.type =3D KAPI_TYPE_FD; + param.constraint_type =3D KAPI_CONSTRAINT_NONE; + + /* Value that overflows int: 0x100000003 -> truncates to 3 */ + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0x100000003LL)); +} + +/* Test 23: ENUM constraint */ +static const s64 test_enum_vals[] =3D { 1, 5, 10 }; + +static void test_constraint_enum(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_enum"; + param.constraint_type =3D KAPI_CONSTRAINT_ENUM; + param.enum_values =3D test_enum_vals; + param.enum_count =3D ARRAY_SIZE(test_enum_vals); + + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 1)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 5)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 10)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 3)); + KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 11)); +} + +/* Test 24: BUFFER constraint always accepts (size checked at runtime) */ +static void test_constraint_buffer(struct kunit *test) +{ + struct kapi_param_spec param =3D {}; + + param.name =3D "test_buf"; + param.constraint_type =3D KAPI_CONSTRAINT_BUFFER; + + /* Buffer constraint doesn't validate the value itself */ + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0)); + KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 4096)); +} + +/* Test 25: RETURN_RANGE check type */ +static void test_return_range(struct kunit *test) +{ + struct kernel_api_spec *spec; + + spec =3D kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + spec->name =3D "test_ret_range"; + spec->return_magic =3D KAPI_MAGIC_RETURN; + spec->return_spec.check_type =3D KAPI_RETURN_RANGE; + spec->return_spec.success_min =3D 0; + spec->return_spec.success_max =3D 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)); +} + +#endif /* CONFIG_KAPI_RUNTIME_CHECKS */ + +/* Test 26: 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"); +} + +/* Test 27: 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 =3D kzalloc_obj(*spec1, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec1); + spec2 =3D 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 =3D kapi_get_spec("multi_spec_1"); + KUNIT_EXPECT_PTR_EQ(test, found, (const struct kernel_api_spec *)spec1); + + found =3D 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); +} + +/* Test 20: JSON export produces valid output */ +static void test_json_export(struct kunit *test) +{ + struct kernel_api_spec *spec; + char *buf; + int ret; + + spec =3D kzalloc_obj(*spec, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + + buf =3D kzalloc(4096, GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf); + + init_test_spec(spec, "test_json"); + spec->param_count =3D 1; + spec->params[0].name =3D "arg0"; + spec->params[0].type_name =3D "int"; + + ret =3D 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] =3D=3D '\n' || buf[ret - 1] =3D=3D ' ')) + 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\"")); + + kfree(buf); + kfree(spec); +} + +/* Test 21: 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 =3D kapi_export_json(NULL, buf, sizeof(buf)); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + + spec =3D kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + init_test_spec(spec, "test"); + + ret =3D kapi_export_json(spec, NULL, 64); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + + ret =3D kapi_export_json(spec, buf, 0); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); +} + +/* Test 22: JSON export with small buffer truncates gracefully */ +static void test_json_export_small_buffer(struct kunit *test) +{ + struct kernel_api_spec *spec; + char buf[64]; + int ret; + + spec =3D kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec); + init_test_spec(spec, "test_small"); + + ret =3D kapi_export_json(spec, buf, sizeof(buf)); + + /* Should return number of bytes written, buffer too small for full JSON = */ + KUNIT_EXPECT_GT(test, ret, 0); + KUNIT_EXPECT_LT(test, ret, (int)sizeof(buf)); +} + +static struct kunit_case kapi_test_cases[] =3D { + 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_empty_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), +#endif + 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 =3D { + .name =3D "kapi", + .test_cases =3D 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..1a9041a7f21a4 --- /dev/null +++ b/kernel/api/kernel_api_spec.c @@ -0,0 +1,1363 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026 Sasha Levin + * + * kernel_api_spec.c - Kernel API Specification Framework Implementation + * + * Provides runtime support for kernel API specifications including valida= tion, + * export to various formats, and querying capabilities. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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_mu= tex + */ +static const struct kernel_api_spec *__kapi_find_spec_locked(const char *n= ame) +{ + const struct kernel_api_spec * const *pp; + struct dynamic_api_spec *dyn_spec; + + for (pp =3D __start_kapi_specs; pp < __stop_kapi_specs; pp++) { + const struct kernel_api_spec *spec =3D *pp; + + if (spec && spec->name && strcmp(spec->name, name) =3D=3D 0) + return spec; + } + + list_for_each_entry(dyn_spec, &dynamic_api_specs, list) { + if (dyn_spec->spec->name && + strcmp(dyn_spec->spec->name, name) =3D=3D 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 + * returned pointer is valid indefinitely for specifications in the + * ``.kapi_specs`` ELF section (built-in, statically defined). For + * dynamically registered specs, the pointer is only valid while the + * caller can guarantee no concurrent kapi_unregister_spec() runs -- + * typically by pinning the providing module or serialising with the + * registrant. This framework has no in-tree dynamic callers yet; any + * future dynamic caller must add refcount- or SRCU-based protection + * for the lookup/unregister race. + * + * 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 =3D __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 =3D 0; + + if (!spec || !spec->name || !spec->name[0]) + return -EINVAL; + + dyn_spec =3D kzalloc_obj(*dyn_spec, GFP_KERNEL); + if (!dyn_spec) + return -ENOMEM; + + dyn_spec->spec =3D spec; + + mutex_lock(&api_spec_mutex); + + /* Check if already exists while holding lock to prevent races */ + if (__kapi_find_spec_locked(spec->name)) { + ret =3D -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) =3D=3D 0) { + list_del(&dyn_spec->list); + kfree(dyn_spec); + break; + } + } + mutex_unlock(&api_spec_mutex); +} +EXPORT_SYMBOL_GPL(kapi_unregister_spec); + +/** + * param_type_to_string - Convert parameter type to string + * @type: Parameter type + * + * Return: String representation of type + */ +static const char *param_type_to_string(enum kapi_param_type type) +{ + static const char * const type_names[] =3D { + [KAPI_TYPE_VOID] =3D "void", + [KAPI_TYPE_INT] =3D "int", + [KAPI_TYPE_UINT] =3D "uint", + [KAPI_TYPE_PTR] =3D "pointer", + [KAPI_TYPE_STRUCT] =3D "struct", + [KAPI_TYPE_UNION] =3D "union", + [KAPI_TYPE_ENUM] =3D "enum", + [KAPI_TYPE_FUNC_PTR] =3D "function_pointer", + [KAPI_TYPE_ARRAY] =3D "array", + [KAPI_TYPE_FD] =3D "file_descriptor", + [KAPI_TYPE_USER_PTR] =3D "user_pointer", + [KAPI_TYPE_PATH] =3D "pathname", + [KAPI_TYPE_CUSTOM] =3D "custom", + }; + + if (type >=3D ARRAY_SIZE(type_names)) + return "unknown"; + + return type_names[type]; +} + +/** + * lock_type_to_string - Convert lock type to string + * @type: Lock type + * + * Return: String representation of lock type + */ +static const char *lock_type_to_string(enum kapi_lock_type type) +{ + static const char * const lock_names[] =3D { + [KAPI_LOCK_NONE] =3D "none", + [KAPI_LOCK_MUTEX] =3D "mutex", + [KAPI_LOCK_SPINLOCK] =3D "spinlock", + [KAPI_LOCK_RWLOCK] =3D "rwlock", + [KAPI_LOCK_SEQLOCK] =3D "seqlock", + [KAPI_LOCK_RCU] =3D "rcu", + [KAPI_LOCK_SEMAPHORE] =3D "semaphore", + [KAPI_LOCK_CUSTOM] =3D "custom", + }; + + if (type >=3D ARRAY_SIZE(lock_names)) + return "unknown"; + + return lock_names[type]; +} + +/** + * lock_scope_to_string - Convert lock scope to string + * @scope: Lock scope + * + * Return: String representation of lock scope + */ +static const char *lock_scope_to_string(enum kapi_lock_scope scope) +{ + static const char * const scope_names[] =3D { + [KAPI_LOCK_INTERNAL] =3D "internal", + [KAPI_LOCK_ACQUIRES] =3D "acquires", + [KAPI_LOCK_RELEASES] =3D "releases", + [KAPI_LOCK_CALLER_HELD] =3D "caller_held", + }; + + if (scope >=3D 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[] =3D { + [KAPI_RETURN_EXACT] =3D "exact", + [KAPI_RETURN_RANGE] =3D "range", + [KAPI_RETURN_ERROR_CHECK] =3D "error_check", + [KAPI_RETURN_FD] =3D "file_descriptor", + [KAPI_RETURN_CUSTOM] =3D "custom", + [KAPI_RETURN_NO_RETURN] =3D "no_return", + }; + + if (type >=3D 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[] =3D { + [KAPI_CAP_BYPASS_CHECK] =3D "bypass_check", + [KAPI_CAP_INCREASE_LIMIT] =3D "increase_limit", + [KAPI_CAP_OVERRIDE_RESTRICTION] =3D "override_restriction", + [KAPI_CAP_GRANT_PERMISSION] =3D "grant_permission", + [KAPI_CAP_MODIFY_BEHAVIOR] =3D "modify_behavior", + [KAPI_CAP_ACCESS_RESOURCE] =3D "access_resource", + [KAPI_CAP_PERFORM_OPERATION] =3D "perform_operation", + }; + + if (action >=3D ARRAY_SIZE(action_names)) + return "unknown"; + + return action_names[action]; +} + +/* + * 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 =3D 0; + const char *p; + + if (!str || size =3D=3D 0) + return 0; + + for (p =3D str; *p && ret < size - 1; p++) { + switch (*p) { + case '\\': + ret +=3D scnprintf(buf + ret, size - ret, "\\\\"); + break; + case '"': + ret +=3D scnprintf(buf + ret, size - ret, "\\\""); + break; + case '\n': + ret +=3D scnprintf(buf + ret, size - ret, "\\n"); + break; + case '\r': + ret +=3D scnprintf(buf + ret, size - ret, "\\r"); + break; + case '\t': + ret +=3D scnprintf(buf + ret, size - ret, "\\t"); + break; + default: + if ((unsigned char)*p < 0x20) { + ret +=3D scnprintf(buf + ret, size - ret, + "\\u%04x", (unsigned char)*p); + } else { + ret +=3D scnprintf(buf + ret, size - ret, + "%c", *p); + } + break; + } + } + + if (ret < size) + buf[ret] =3D '\0'; + + return ret; +} + +/* Helper to write a JSON-escaped string field */ +static int kapi_json_str(char *buf, size_t size, const char *str) +{ + int ret =3D 0; + + ret +=3D scnprintf(buf, size, "\""); + ret +=3D kapi_json_escape(buf + ret, size - ret, str); + ret +=3D scnprintf(buf + ret, size - ret, "\""); + 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 or negative error + */ +int kapi_export_json(const struct kernel_api_spec *spec, char *buf, size_t= size) +{ + int ret =3D 0; + int i; + + if (!spec || !buf || size =3D=3D 0) + return -EINVAL; + + ret =3D scnprintf(buf, size, "{\n \"name\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, spec->name); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"version\": %u,\n \"description\": ", + spec->version); + ret +=3D kapi_json_str(buf + ret, size - ret, spec->description); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"long_description\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, spec->long_description); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"context_flags\": \"0x%x\",\n", + spec->context_flags); + + /* Parameters */ + ret +=3D scnprintf(buf + ret, size - ret, " \"parameters\": [\n"); + + for (i =3D 0; i < spec->param_count && i < KAPI_MAX_PARAMS; i++) { + const struct kapi_param_spec *param =3D &spec->params[i]; + + ret +=3D scnprintf(buf + ret, size - ret, " {\n \"name\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, param->name); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"type\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, param->type_name); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"type_class\": \"%s\",\n \"flags\": \"0x%x\",\n \"= description\": ", + param_type_to_string(param->type), + param->flags); + ret +=3D kapi_json_str(buf + ret, size - ret, param->description); + ret +=3D scnprintf(buf + ret, size - ret, + "\n }%s\n", + (i < spec->param_count - 1) ? "," : ""); + } + + ret +=3D scnprintf(buf + ret, size - ret, " ],\n"); + + /* Return value */ + ret +=3D scnprintf(buf + ret, size - ret, " \"return\": {\n \"type\":= "); + ret +=3D kapi_json_str(buf + ret, size - ret, spec->return_spec.type_name= ); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"type_class\": \"%s\",\n \"check_type\": \"%s\",\n", + param_type_to_string(spec->return_spec.type), + return_check_type_to_string(spec->return_spec.check_type)); + + switch (spec->return_spec.check_type) { + case KAPI_RETURN_EXACT: + ret +=3D scnprintf(buf + ret, size - ret, + " \"success_value\": %lld,\n", + spec->return_spec.success_value); + break; + case KAPI_RETURN_RANGE: + ret +=3D scnprintf(buf + ret, size - ret, + " \"success_min\": %lld,\n \"success_max\": %lld,\n", + spec->return_spec.success_min, + spec->return_spec.success_max); + break; + case KAPI_RETURN_ERROR_CHECK: + ret +=3D scnprintf(buf + ret, size - ret, + " \"error_count\": %u,\n", + spec->return_spec.error_count); + break; + default: + break; + } + + ret +=3D scnprintf(buf + ret, size - ret, " \"description\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, spec->return_spec.descripti= on); + ret +=3D scnprintf(buf + ret, size - ret, "\n },\n"); + + /* Errors */ + ret +=3D scnprintf(buf + ret, size - ret, " \"errors\": [\n"); + + for (i =3D 0; i < spec->error_count && i < KAPI_MAX_ERRORS; i++) { + const struct kapi_error_spec *error =3D &spec->errors[i]; + + ret +=3D scnprintf(buf + ret, size - ret, + " {\n \"code\": %d,\n \"name\": ", + error->error_code); + ret +=3D kapi_json_str(buf + ret, size - ret, error->name); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"condition\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, error->condition); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"description\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, error->description); + ret +=3D scnprintf(buf + ret, size - ret, + "\n }%s\n", + (i < spec->error_count - 1) ? "," : ""); + } + + ret +=3D scnprintf(buf + ret, size - ret, " ],\n"); + + /* Locks */ + ret +=3D scnprintf(buf + ret, size - ret, " \"locks\": [\n"); + + for (i =3D 0; i < spec->lock_count && i < KAPI_MAX_LOCKS; i++) { + const struct kapi_lock_spec *lock =3D &spec->locks[i]; + + ret +=3D scnprintf(buf + ret, size - ret, " {\n \"name\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, lock->lock_name); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"type\": \"%s\",\n \"scope\": \"%s\",\n \"descript= ion\": ", + lock_type_to_string(lock->lock_type), + lock_scope_to_string(lock->scope)); + ret +=3D kapi_json_str(buf + ret, size - ret, lock->description); + ret +=3D scnprintf(buf + ret, size - ret, + "\n }%s\n", + (i < spec->lock_count - 1) ? "," : ""); + } + + ret +=3D scnprintf(buf + ret, size - ret, " ],\n"); + + /* Capabilities */ + ret +=3D scnprintf(buf + ret, size - ret, " \"capabilities\": [\n"); + + for (i =3D 0; i < spec->capability_count && i < KAPI_MAX_CAPABILITIES; i+= +) { + const struct kapi_capability_spec *cap =3D &spec->capabilities[i]; + + ret +=3D scnprintf(buf + ret, size - ret, + " {\n \"capability\": %d,\n \"name\": ", + cap->capability); + ret +=3D kapi_json_str(buf + ret, size - ret, cap->cap_name); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"action\": \"%s\",\n \"allows\": ", + capability_action_to_string(cap->action)); + ret +=3D kapi_json_str(buf + ret, size - ret, cap->allows); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"without_cap\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, cap->without_cap); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"check_condition\":= "); + ret +=3D kapi_json_str(buf + ret, size - ret, cap->check_condition); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"priority\": %u", c= ap->priority); + + if (cap->alternative_count > 0) { + int j; + + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"alternatives\": ["); + for (j =3D 0; j < cap->alternative_count; j++) { + ret +=3D scnprintf(buf + ret, size - ret, + "%d%s", cap->alternative[j], + (j < cap->alternative_count - 1) ? ", " : ""); + } + ret +=3D scnprintf(buf + ret, size - ret, "]"); + } + + ret +=3D scnprintf(buf + ret, size - ret, + "\n }%s\n", + (i < spec->capability_count - 1) ? "," : ""); + } + + ret +=3D scnprintf(buf + ret, size - ret, " ],\n"); + + /* Constraints */ + ret +=3D scnprintf(buf + ret, size - ret, " \"constraints\": [\n"); + for (i =3D 0; i < spec->constraint_count && i < KAPI_MAX_CONSTRAINTS; i++= ) { + const struct kapi_constraint_spec *con =3D &spec->constraints[i]; + + ret +=3D scnprintf(buf + ret, size - ret, " {\n \"name\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, con->name); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"description\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, con->description); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"expression\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, con->expression); + ret +=3D scnprintf(buf + ret, size - ret, + "\n }%s\n", + (i < spec->constraint_count - 1) ? "," : ""); + } + ret +=3D scnprintf(buf + ret, size - ret, " ],\n"); + + /* Signals */ + ret +=3D scnprintf(buf + ret, size - ret, " \"signals\": [\n"); + for (i =3D 0; i < spec->signal_count && i < KAPI_MAX_SIGNALS; i++) { + const struct kapi_signal_spec *sig =3D &spec->signals[i]; + + ret +=3D scnprintf(buf + ret, size - ret, + " {\n \"signal_num\": %d,\n \"signal_name\": ", + sig->signal_num); + ret +=3D kapi_json_str(buf + ret, size - ret, sig->signal_name); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"direction\": \"0x%x\",\n \"action\": %u,\n \"targ= et\": ", + sig->direction, sig->action); + ret +=3D kapi_json_str(buf + ret, size - ret, sig->target); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"condition\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, sig->condition); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"description\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, sig->description); + ret +=3D 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 +=3D kapi_json_str(buf + ret, size - ret, sig->timing); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"priority\": %u,\n \"interruptible\": %s,\n \"queu= e_behavior\": ", + sig->priority, + sig->interruptible ? "true" : "false"); + ret +=3D kapi_json_str(buf + ret, size - ret, sig->queue_behavior); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"state_required\": \"0x%x\",\n \"state_forbidden\": \"0= x%x\"\n }%s\n", + sig->state_required, + sig->state_forbidden, + (i < spec->signal_count - 1) ? "," : ""); + } + ret +=3D scnprintf(buf + ret, size - ret, " ],\n"); + + /* Side effects */ + ret +=3D scnprintf(buf + ret, size - ret, " \"side_effects\": [\n"); + for (i =3D 0; i < spec->side_effect_count && i < KAPI_MAX_SIDE_EFFECTS; i= ++) { + const struct kapi_side_effect *eff =3D &spec->side_effects[i]; + + ret +=3D scnprintf(buf + ret, size - ret, + " {\n \"type\": \"0x%x\",\n \"target\": ", + eff->type); + ret +=3D kapi_json_str(buf + ret, size - ret, eff->target); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"condition\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, eff->condition); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"description\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, eff->description); + ret +=3D scnprintf(buf + ret, size - ret, + ",\n \"reversible\": %s\n }%s\n", + eff->reversible ? "true" : "false", + (i < spec->side_effect_count - 1) ? "," : ""); + } + ret +=3D scnprintf(buf + ret, size - ret, " ],\n"); + + /* Additional info */ + ret +=3D scnprintf(buf + ret, size - ret, " \"examples\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, spec->examples); + ret +=3D scnprintf(buf + ret, size - ret, ",\n \"notes\": "); + ret +=3D kapi_json_str(buf + ret, size - ret, spec->notes); + ret +=3D scnprintf(buf + ret, size - ret, "\n}\n"); + + return ret; +} +EXPORT_SYMBOL_GPL(kapi_export_json); + + +/** + * kapi_print_spec - Print API specification to kernel log + * @spec: API specification to print + */ +void kapi_print_spec(const struct kernel_api_spec *spec) +{ + int i; + + if (!spec) + return; + + pr_info("=3D=3D=3D Kernel API Specification =3D=3D=3D\n"); + pr_info("Name: %s\n", spec->name); + pr_info("Version: %u\n", spec->version); + pr_info("Description: %s\n", spec->description); + + if (spec->long_description && *spec->long_description) + pr_info("Long Description: %s\n", spec->long_description); + + pr_info("Context Flags: 0x%x\n", spec->context_flags); + + /* Parameters */ + if (spec->param_count > 0) { + pr_info("Parameters:\n"); + for (i =3D 0; i < spec->param_count && i < KAPI_MAX_PARAMS; i++) { + const struct kapi_param_spec *param =3D &spec->params[i]; + + pr_info(" [%d] %s: %s (flags: 0x%x)\n", + i, param->name, param->type_name, param->flags); + if (param->description && *param->description) + pr_info(" Description: %s\n", param->description); + } + } + + /* Return value */ + pr_info("Return: %s\n", spec->return_spec.type_name); + if (spec->return_spec.description && *spec->return_spec.description) + pr_info(" Description: %s\n", spec->return_spec.description); + + /* Errors */ + if (spec->error_count > 0) { + pr_info("Possible Errors:\n"); + for (i =3D 0; i < spec->error_count && i < KAPI_MAX_ERRORS; i++) { + const struct kapi_error_spec *error =3D &spec->errors[i]; + + pr_info(" %s (%d): %s\n", + error->name, error->error_code, error->condition); + } + } + + /* Capabilities */ + if (spec->capability_count > 0) { + pr_info("Capabilities:\n"); + for (i =3D 0; i < spec->capability_count && i < KAPI_MAX_CAPABILITIES; i= ++) { + const struct kapi_capability_spec *cap =3D &spec->capabilities[i]; + + pr_info(" %s (%d):\n", cap->cap_name, cap->capability); + pr_info(" Action: %s\n", capability_action_to_string(cap->action)); + pr_info(" Allows: %s\n", cap->allows); + pr_info(" Without: %s\n", cap->without_cap); + if (cap->check_condition && *cap->check_condition) + pr_info(" Condition: %s\n", cap->check_condition); + } + } + + pr_info("=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D\n"); +} +EXPORT_SYMBOL_GPL(kapi_print_spec); + +#ifdef CONFIG_KAPI_RUNTIME_CHECKS + +/** + * kapi_validate_fd - Validate that a file descriptor value is in valid ra= nge + * @fd: File descriptor to validate + * + * Only checks the numeric range, not whether the fd is currently open. + * Checking openness would be TOCTOU (the fd can be closed between check + * and use) and incorrect for close() (which should accept any fd, returni= ng + * EBADF for invalid ones). The kernel's own syscall handling validates + * actual fd state. + * + * Return: true if fd is in valid range, false otherwise + */ +static bool kapi_validate_fd(int fd) +{ + /* Special case: AT_FDCWD is always valid */ + if (fd =3D=3D AT_FDCWD) + return true; + + /* Check basic range - negative fds are invalid */ + if (fd < 0) + return false; + + return true; +} + +/** + * 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_spe= c *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 =3D all_params[param_spec->size_param_idx - 1]; + + /* Validate count is non-negative */ + if (count < 0) { + pr_warn_ratelimited("Parameter %s: size determinant is negative (%lld)\= n", + param_spec->name, count); + return false; + } + + /* Check for multiplication overflow */ + if (param_spec->size_multiplier > 0 && + count > SIZE_MAX / param_spec->size_multiplier) { + pr_warn_ratelimited("Parameter %s: size calculation overflow\n", + param_spec->name); + return false; + } + + actual_size =3D (size_t)count * param_spec->size_multiplier; + } else { + /* Use fixed size */ + actual_size =3D 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; + } + + /* Check if the path is accessible */ + 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 =3D strnlen_user(path, PATH_MAX + 1); + if (len =3D=3D 0) { + pr_warn_ratelimited("Parameter %s: invalid path pointer %p\n", + param_spec->name, path); + return false; + } + + /* Check path length limit */ + 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_spe= c->name); + return false; + } + + /* Check if the string pointer is accessible */ + 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 =3D param_spec->max_value > 0 ? + (size_t)param_spec->max_value + 1 : PATH_MAX + 1; + len =3D strnlen_user(str, max_check_len); + + if (len =3D=3D 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--; + + /* Check minimum length constraint */ + 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; + } + + /* Check maximum length constraint */ + 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 s= ize + * @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_sp= ec->name); + return false; + } + + /* Validate size is specified */ + if (param_spec->size =3D=3D 0) { + pr_warn_ratelimited("Parameter %s: size not specified for user pointer v= alidation\n", + param_spec->name); + return false; + } + + /* Check if the memory region is accessible */ + if (!access_ok(ptr, param_spec->size)) { + pr_warn_ratelimited("Parameter %s: user pointer %p not accessible for %z= u bytes\n", + param_spec->name, ptr, param_spec->size); + return false; + } + + 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 val= ue) +{ + int i; + + /* Special handling for file descriptor type */ + if (param_spec->type =3D=3D 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 rang= e\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; + } + /* Continue with additional constraint checks if needed */ + } + + /* Special handling for user pointer type */ + if (param_spec->type =3D=3D KAPI_TYPE_USER_PTR) { + const void __user *ptr =3D (const void __user *)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; + } + /* Continue with additional constraint checks if needed */ + } + + /* Special handling for path type */ + if (param_spec->type =3D=3D KAPI_TYPE_PATH) { + const char __user *path =3D (const char __user *)value; + + if (!kapi_validate_path(path, param_spec)) + return false; + /* Continue with additional constraint checks if needed */ + } + + switch (param_spec->constraint_type) { + case KAPI_CONSTRAINT_NONE: + case KAPI_CONSTRAINT_BUFFER: + return true; + + case KAPI_CONSTRAINT_RANGE: + /* + * If max_value is negative, it was likely set from an unsigned + * constant (e.g. SIZE_MAX) that overflowed s64. Treat as no + * upper bound =E2=80=94 only check the minimum. + */ + if (param_spec->max_value >=3D 0) { + 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 (v= alid 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 =3D=3D 0) + return true; + + for (i =3D 0; i < param_spec->enum_count; i++) { + if (value =3D=3D 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 =3D=3D 0) { + pr_warn_ratelimited("Parameter %s: alignment constraint specified but a= lignment 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 bound= ary\n", + param_spec->name, value, param_spec->alignment); + return false; + } + return true; + + case KAPI_CONSTRAINT_POWER_OF_TWO: + if (value =3D=3D 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_S= IZE=3D%ld)\n", + param_spec->name, value, PAGE_SIZE); + return false; + } + return true; + + case KAPI_CONSTRAINT_NONZERO: + if (value =3D=3D 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 *)value, param_spec); + + case KAPI_CONSTRAINT_USER_PATH: + return kapi_validate_path((const char __user *)value, param_spec); + + case KAPI_CONSTRAINT_USER_PTR: + return kapi_validate_user_ptr_constraint((const void __user *)value, par= am_spec); + + case KAPI_CONSTRAINT_CUSTOM: + if (param_spec->validate) + return param_spec->validate(value); + return true; + + default: + return true; + } +} +EXPORT_SYMBOL_GPL(kapi_validate_param); + +/** + * kapi_validate_param_with_context - Validate parameter with access to al= l 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 =3D=3D KAPI_TYPE_USER_PTR) { + const void __user *ptr =3D (const void __user *)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, par= am_count)) { + pr_warn_ratelimited("Parameter %s: invalid user pointer %p\n", + param_spec->name, ptr); + return false; + } + /* Continue with additional constraint checks if needed */ + } + + /* 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_param - Validate syscall parameter with enforceme= nt + * @spec: API specification + * @param_idx: Parameter index + * @value: Parameter value + * + * Return: -EINVAL if invalid, 0 if valid + */ +int kapi_validate_syscall_param(const struct kernel_api_spec *spec, + int param_idx, s64 value) +{ + const struct kapi_param_spec *param_spec; + + if (!spec || param_idx < 0 || param_idx >=3D spec->param_count) + return 0; + + param_spec =3D &spec->params[param_idx]; + + if (!kapi_validate_param(param_spec, value)) { + if (strncmp(spec->name, "sys_", 4) =3D=3D 0) { + /* For syscalls, we can return EINVAL to userspace */ + return -EINVAL; + } + } + + return 0; +} +EXPORT_SYMBOL_GPL(kapi_validate_syscall_param); + +/** + * kapi_validate_syscall_params - Validate all syscall parameters together + * @spec: API specification + * @params: Array of parameter values + * @param_count: Number of parameters + * + * Return: -EINVAL if any parameter is invalid, 0 if all valid + */ +int kapi_validate_syscall_params(const struct kernel_api_spec *spec, + const s64 *params, int param_count) +{ + int i; + + if (!spec || !params) + return 0; + + /* Validate that we have the expected number of parameters */ + if (param_count !=3D spec->param_count) { + pr_warn_ratelimited("API %s: parameter count mismatch (expected %u, got = %d)\n", + spec->name, spec->param_count, param_count); + return -EINVAL; + } + + /* Validate each parameter with context */ + for (i =3D 0; i < spec->param_count && i < KAPI_MAX_PARAMS; i++) { + const struct kapi_param_spec *param_spec =3D &spec->params[i]; + + if (!kapi_validate_param_with_context(param_spec, params[i], params, par= am_count)) { + if (strncmp(spec->name, "sys_", 4) =3D=3D 0) { + /* For syscalls, we can return EINVAL to userspace */ + return -EINVAL; + } + } + } + + return 0; +} +EXPORT_SYMBOL_GPL(kapi_validate_syscall_params); + +/** + * kapi_check_return_success - Check if return value indicates success + * @return_spec: Return specification + * @retval: Return value to check + * + * Returns true if the return value indicates success according to the spe= c. + */ +bool kapi_check_return_success(const struct kapi_return_spec *return_spec,= s64 retval) +{ + u32 i; + + if (!return_spec) + return true; /* No spec means we can't validate */ + + switch (return_spec->check_type) { + case KAPI_RETURN_EXACT: + return retval =3D=3D return_spec->success_value; + + case KAPI_RETURN_RANGE: + return retval >=3D return_spec->success_min && + retval <=3D return_spec->success_max; + + case KAPI_RETURN_ERROR_CHECK: + /* Success if NOT in error list */ + if (return_spec->error_values) { + for (i =3D 0; i < return_spec->error_count; i++) { + if (retval =3D=3D return_spec->error_values[i]) + return false; /* Found in error list */ + } + } + return true; /* Not in error list =3D success */ + + case KAPI_RETURN_FD: + /* File descriptors: >=3D 0 is success, < 0 is error */ + return retval >=3D 0; + + case KAPI_RETURN_CUSTOM: + if (return_spec->is_success) + return return_spec->is_success(retval); + fallthrough; + + default: + return true; /* Unknown check type, assume success */ + } +} +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: true if return value is valid according to spec, false otherwis= e. + * + * This function checks: + * 1. If the value indicates success, it must match the success criteria + * 2. If the value indicates error, it must be one of the specified error = codes + */ +bool kapi_validate_return_value(const struct kernel_api_spec *spec, s64 re= tval) +{ + 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 =3D kapi_check_return_success(&spec->return_spec, retval); + + if (is_success) { + /* Special validation for file descriptor returns */ + if (spec->return_spec.check_type =3D=3D KAPI_RETURN_FD) { + /* For successful FD returns, validate it's a valid 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; + } + + /* Error case - check if it's one of the specified errors */ + if (spec->error_count =3D=3D 0) { + /* No errors specified, so any error is potentially valid */ + pr_debug("API %s returned unspecified error %lld\n", + spec->name, retval); + return true; + } + + /* Check if the error is in our list of specified errors */ + for (i =3D 0; i < spec->error_count && i < KAPI_MAX_ERRORS; i++) { + if (retval =3D=3D 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 with enfor= cement + * @spec: API specification + * @retval: Return value + * + * Return: 0 if valid, -EINVAL if the return value doesn't match spec + * + * For syscalls, this can help detect kernel bugs where unspecified error + * codes are returned to userspace. + */ +int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 r= etval) +{ + if (!spec) + return 0; + + /* Skip return validation if return spec was not defined */ + if (spec->return_magic !=3D KAPI_MAGIC_RETURN) + return 0; + + if (!kapi_validate_return_value(spec, retval)) { + /* Log the violation but don't change the return value */ + pr_warn_ratelimited("KAPI: Syscall %s returned unspecified value %lld\n", + spec->name, retval); + } + + return 0; +} +EXPORT_SYMBOL_GPL(kapi_validate_syscall_return); + +/** + * kapi_check_context - Check if current context matches API requirements + * @spec: API specification to check against + */ +void kapi_check_context(const struct kernel_api_spec *spec) +{ + bool valid =3D false; + u32 ctx; + + if (!spec) + return; + + ctx =3D spec->context_flags; + + if (!ctx) + return; + + /* Check if we're in an allowed context */ + if ((ctx & KAPI_CTX_PROCESS) && !in_interrupt()) + valid =3D true; + + if ((ctx & KAPI_CTX_SOFTIRQ) && in_softirq()) + valid =3D true; + + if ((ctx & KAPI_CTX_HARDIRQ) && in_hardirq()) + valid =3D true; + + if ((ctx & KAPI_CTX_NMI) && in_nmi()) + valid =3D 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 */ --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C33EC2DA749; Fri, 29 May 2026 23:33:21 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097605; cv=none; b=C/k/XP4jWLHp9JpcjgYaAsfyvYVeEga0bhRcyN3NBOhHkQI+H15f8v4VT4Nykzrl6+MpLNO9Wta1wXdofRLBxbrZXRX+MfdqNTevaXrXrY6OBDYm0cvtBEObo6EAZsqZsRzii5vlUNqOQ7NHvT2/DaNhsHPWDSh6T+TG+vMQWrM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097605; c=relaxed/simple; bh=QqAwJ8hyF9GwFAccsnbFKxyXNr0st69ZQwvvt66tofY=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=UYQ6c5S6v5eKc9DxPPdZZTdRSFPW/NlmVfyWcB5NucAUZVwqXMKiaIVS9hMdm9bGRy926QK5DAFsqY27Qp/k6GXzj4kCjXv1PB2UaS0e5NVFINBCEYALLeo8cO/reWLTbmkixZ6ilFYR0Gq2yirC4ecgFomQPyy8j6nr2+1eWSg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=Xqqs+YuK; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="Xqqs+YuK" Received: by smtp.kernel.org (Postfix) with ESMTPSA id A7D201F00898; Fri, 29 May 2026 23:33:18 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097601; bh=Z8E8+YUT++HxjqJUJXgJ0AGt6T4/j5NW4EKj6ZYvNHY=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=Xqqs+YuKyctadZ/CSaEo0EafLC7xHxOSPboqtTFqZCXrGlTWfFW8GPcdcdXPBxtlt akQ+fjGhq6HM3IDGXsl5zIIG9oLeEU0cxSO+n5iMiQoSmzOf52T89oCpkWelFGPtZC xfTXvSIdV/oEfeS4a6RGDTdZLgvOCQLa6ldYUkGU3XxRlmJsh9agSfz2PF3wkuQj65 GhcsMMpMvPBfI0f76AUKlo2wQv97e1q4fR9umHYWp4UbMTxA5upZDvQm7JN0vgbgUN lOgwOPVlZU/M1/JiOvJ7jW+4cmCAx5N5oXY87UyLoKPSv1jTdVgTv/BJPJl58MQ0TV iR9AIpgSW8fiA== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 02/11] kernel/api: enable kerneldoc-based API specifications Date: Fri, 29 May 2026 19:33:01 -0400 Message-ID: <20260529233311.1901670-3-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable This patch adds support for extracting API specifications from kernel-doc comments and generating C macro invocations for the kernel API specification framework. Changes include: - New kdoc_apispec.py module for generating API spec macros - Updates to kernel-doc.py to support -apispec output format - Build system integration in Makefile.build - Generator script for collecting all API specifications - Support for API-specific sections in kernel-doc comments Signed-off-by: Sasha Levin --- Documentation/dev-tools/kernel-api-spec.rst | 24 + Makefile | 1 + scripts/Makefile.build | 20 + tools/docs/kernel-doc | 5 + tools/lib/python/kdoc/kdoc_apispec.py | 1313 +++++++++++++++++++ tools/lib/python/kdoc/kdoc_output.py | 9 +- tools/lib/python/kdoc/kdoc_parser.py | 86 +- 7 files changed, 1453 insertions(+), 5 deletions(-) create mode 100644 tools/lib/python/kdoc/kdoc_apispec.py diff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/de= v-tools/kernel-api-spec.rst index 395c2294d5209..62a1248746622 100644 --- a/Documentation/dev-tools/kernel-api-spec.rst +++ b/Documentation/dev-tools/kernel-api-spec.rst @@ -161,6 +161,19 @@ DSL reference: ``page_aligned``, ``nonzero``, ``custom(fn_name)``. The function-call form populates the matching aux fields (``range:`` / ``valid-mask:`` / ``size-param:``). +* ``arch-mask:`` =E2=80=94 extends a ``mask(...)`` constraint with bits th= at + are valid only on a named architecture. The form is + ``arch-mask: =3D `` 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 genuinely arch-only PROT/MAP bits whose UAPI symbols + are defined only on the matching arch. * ``lock: =E2=80=A6 type:`` accepts ``mutex``, ``spinlock``, ``rwlock``, ``seqlock``, ``rcu``, ``semaphore``, ``custom`` or ``KAPI_LOCK_*``. * ``signal: =E2=80=A6 direction:`` accepts ``receive``, ``send``, ``handle= ``, @@ -239,6 +252,17 @@ execution context, and return values. Parameter violat= ions are reported via ``pr_warn_ratelimited`` and return value violations via ``WARN_ONCE`` to a= void flooding the kernel log. =20 +.. 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 ----------------- =20 diff --git a/Makefile b/Makefile index 36d0a32fbe49b..133482902cb44 100644 --- a/Makefile +++ b/Makefile @@ -2126,6 +2126,7 @@ clean: $(clean-dirs) -o -name '*.c.[012]*.*' \ -o -name '*.ll' \ -o -name '*.gcno' \ + -o -name '*.apispec.h' \ \) -type f -print \ -o -name '.tmp_*' -print \ | xargs rm -rf diff --git a/scripts/Makefile.build b/scripts/Makefile.build index 3652b85be5459..0272b714897ee 100644 --- a/scripts/Makefile.build +++ b/scripts/Makefile.build @@ -174,6 +174,26 @@ ifneq ($(KBUILD_EXTRA_WARN),) endif endif =20 +ifeq ($(CONFIG_KAPI_SPEC),y) +has-apispec =3D $(shell grep -lE '^[[:space:]]*\*[[:space:]]*(contexts|con= text-flags|context):' $(1) 2>/dev/null) +apispec-c-files :=3D $(call has-apispec, \ + $(patsubst $(obj)/%.o,$(src)/%.c, \ + $(filter-out %/built-in.a,$(real-obj-y) $(real-obj-m)))) +apispec-y :=3D $(patsubst $(src)/%.c,$(obj)/%.apispec.h,$(apispec-c-files)) +always-y +=3D $(apispec-y) +targets +=3D $(apispec-y) + +quiet_cmd_apispec =3D APISPEC $@ + cmd_apispec =3D PYTHONDONTWRITEBYTECODE=3D1 $(KERNELDOC) -apispec \ + $(KDOCFLAGS) $< > $@ || rm -f $@ + +$(obj)/%.apispec.h: $(src)/%.c $(KERNELDOC) FORCE + $(call if_changed,apispec) + +$(apispec-y:.apispec.h=3D.o): $(obj)/%.o: $(obj)/%.apispec.h +$(apispec-y:.apispec.h=3D.o): private c_flags +=3D -include $(obj)/$*.apis= pec.h +endif + # Compile C sources (.c) # ------------------------------------------------------------------------= --- =20 diff --git a/tools/docs/kernel-doc b/tools/docs/kernel-doc index aed09f9a54dd1..e71e663d9b7c0 100755 --- a/tools/docs/kernel-doc +++ b/tools/docs/kernel-doc @@ -253,6 +253,8 @@ def main(): help=3D"Output reStructuredText format (default).= ") out_fmt.add_argument("-N", "-none", "--none", action=3D"store_true", help=3D"Do not output documentation, only warning= s.") + out_fmt.add_argument("-apispec", "--apispec", action=3D"store_true", + help=3D"Output C macro invocations for kernel API= specifications.") =20 # # Output selection mutually-exclusive group @@ -323,11 +325,14 @@ def main(): # from kdoc.kdoc_files import KernelFiles # pylint: disable= =3DC0415 from kdoc.kdoc_output import RestFormat, ManFormat # pylint: disable= =3DC0415 + from kdoc.kdoc_apispec import ApiSpecFormat # pylint: disable= =3DC0415 =20 if args.man: out_style =3D ManFormat(modulename=3Dargs.modulename) elif args.none: out_style =3D None + elif args.apispec: + out_style =3D ApiSpecFormat() else: out_style =3D RestFormat() =20 diff --git a/tools/lib/python/kdoc/kdoc_apispec.py b/tools/lib/python/kdoc/= kdoc_apispec.py new file mode 100644 index 0000000000000..a6a4c9fd45913 --- /dev/null +++ b/tools/lib/python/kdoc/kdoc_apispec.py @@ -0,0 +1,1313 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 Sasha Levin + +""" +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 introduced in commit 9688de5c25bed. +""" + +from kdoc.kdoc_output import OutputFormat +import re +import sys + + +# Maximum string lengths (from kernel_api_spec.h) +KAPI_MAX_DESC_LEN =3D 512 +# Note: long_description, notes, and examples are stored as pointers to +# .rodata in the generated spec, so they are not truncated by this tool. +KAPI_MAX_NAME_LEN =3D 128 +KAPI_MAX_SIGNAL_NAME_LEN =3D 32 + +# Valid KAPI effect types +VALID_EFFECT_TYPES =3D { + 'KAPI_EFFECT_NONE', 'KAPI_EFFECT_MODIFY_STATE', 'KAPI_EFFECT_PROCESS_S= TATE', + 'KAPI_EFFECT_IRREVERSIBLE', 'KAPI_EFFECT_SCHEDULE', 'KAPI_EFFECT_FILES= YSTEM', + 'KAPI_EFFECT_HARDWARE', 'KAPI_EFFECT_ALLOC_MEMORY', 'KAPI_EFFECT_FREE_= MEMORY', + 'KAPI_EFFECT_SIGNAL_SEND', 'KAPI_EFFECT_FILE_POSITION', 'KAPI_EFFECT_L= OCK_ACQUIRE', + 'KAPI_EFFECT_LOCK_RELEASE', 'KAPI_EFFECT_RESOURCE_CREATE', 'KAPI_EFFEC= T_RESOURCE_DESTROY', + 'KAPI_EFFECT_NETWORK' +} + +# DSL aliases mapping short tokens to their canonical KAPI_* C +# identifier. Unknown tokens pass through unchanged. +_CTX_ALIASES =3D { + '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 =3D { + '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 =3D { + '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 =3D 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 =3D ',' if ',' in expr and '|' not in expr else '|' + tokens =3D [_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 =3D ',' if ',' in expr and '|' not in expr else '|' + tokens =3D [_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 =3D { + '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 =3D { + 'receive': 'KAPI_SIGNAL_RECEIVE', + 'send': 'KAPI_SIGNAL_SEND', + 'handle': 'KAPI_SIGNAL_HANDLE', + 'block': 'KAPI_SIGNAL_BLOCK', + 'ignore': 'KAPI_SIGNAL_IGNORE', +} +_SIGNAL_ACTION_ALIASES =3D { + '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 =3D { + 'before': 'KAPI_SIGNAL_TIME_BEFORE', + 'during': 'KAPI_SIGNAL_TIME_DURING', + 'after': 'KAPI_SIGNAL_TIME_AFTER', +} +_EFFECT_ALIASES =3D { + '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 =3D { + '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//) 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 =3D { + '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 =3D ',' if ',' in expr and '|' not in expr else '|' + tokens =3D [_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 =3D {'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 =3D [p.strip() for p in value.split(',') if p.strip()] + if not parts: + return None, [] + ty =3D _canon_token(parts[0], _TYPE_ALIASES) + flags =3D [_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 =3D value.strip() + if not t: + return None + # Split NAME ( ARGS ) + lp =3D t.find('(') + rp =3D t.rfind(')') + if lp > 0 and rp > lp: + name =3D t[:lp].strip() + args_raw =3D t[lp + 1:rp].strip() + elif lp < 0: + name =3D t + args_raw =3D None + else: + return None + # Bareword must be a single identifier; multi-word values are free tex= t. + if not name or any(c.isspace() for c in name): + return None + key =3D name.lower() + table =3D { + 'range': ('KAPI_CONSTRAINT_RANGE', 'param-range'), + 'mask': ('KAPI_CONSTRAINT_MASK', 'param-mask'), + 'enum': ('KAPI_CONSTRAINT_ENUM', 'param-enum-valu= es'), + '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-par= am'), + 'custom': ('KAPI_CONSTRAINT_CUSTOM', 'param-constrai= nt'), + } + if key not in table: + return None + ctype, aux_key =3D table[key] + extras =3D {} + if aux_key and args_raw is not None: + extras[aux_key] =3D args_raw + return ctype, extras + + +class ApiSpecFormat(OutputFormat): + """Generate C macro invocations for kernel API specifications""" + + def __init__(self): + super().__init__() + self.header_written =3D False + + def msg(self, fname, name, args): + """Handles a single entry from kernel-doc parser""" + if not self.header_written: + header =3D self._generate_header() + self.header_written =3D True + else: + header =3D "" + + self.data =3D "" + result =3D 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 \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n\n" + ) + + def _format_macro_param(self, value, max_len=3DKAPI_MAX_DESC_LEN): + """Format a value for use in C macro parameter. + + Pass max_len=3D0 (or a false-y value) to disable truncation -- use= d for + free-form fields like long_description/notes/examples where the ke= rnel + stores a pointer to .rodata rather than a fixed-size buffer. + """ + if value is None: + return '""' + value =3D str(value).replace('\\', '\\\\').replace('"', '\\"') + value =3D value.replace('\n', ' ').replace('\t', ' ').replace('\r'= , '') + value =3D value.replace('\0', '') + if max_len and len(value) > max_len - 1: + sys.stderr.write( + f"kdoc_apispec: truncating field to {max_len} chars " + f"(original length {len(value)})\n" + ) + value =3D value[:max_len - 4] + '...' + return f'"{value}"' + + def _get_section(self, sections, key): + """Get first line from sections, checking with and without @ prefi= x and case variants""" + for variant in [key, key.capitalize(), key.title()]: + for prefix in ['', '@']: + full_key =3D prefix + variant + if full_key in sections: + content =3D 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 an= d case variants""" + for variant in [key, key.capitalize(), key.title()]: + for prefix in ['', '@']: + full_key =3D prefix + variant + if full_key in sections: + return sections[full_key] + return '' + + def _get_multiline_section(self, sections, key): + """Get full multi-line section content, joined into a single strin= g. + + This is used for fields like notes, long-desc, and examples that + can span multiple lines in the kerneldoc comment. + """ + content =3D self._get_raw_section(sections, key) + if not content: + return None + + # Split into lines, strip each, and join with space + lines =3D content.strip().split('\n') + # Join lines, preserving paragraph breaks (double newlines become = single space) + result =3D ' '.join(line.strip() for line in lines if line.strip()) + return result if result 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 retu= rns (item, next_index) + + Returns: + List of parsed items + """ + if not section_content: + return [] + + items =3D [] + lines =3D section_content.strip().split('\n') + i =3D 0 + + while i < len(lines): + if not lines[i].strip(): + i +=3D 1 + continue + + # Check if this is a main item (not indented) + if not lines[i].startswith((' ', '\t')): + item, i =3D item_parser(lines, i) + if item: + items.append(item) + else: + i +=3D 1 + + return items + + def _parse_subfields(self, lines, start_idx): + """Parse indented subfields starting from start_idx+1. + + Returns: (dict of subfields, next index) + """ + subfields =3D {} + i =3D start_idx + 1 + + current_key =3D None + while i < len(lines) and (lines[i].startswith((' ', '\t'))): + line =3D lines[i].strip() + if ':' in line: + key, value =3D line.split(':', 1) + current_key =3D key.strip() + subfields[current_key] =3D value.strip() + elif current_key and line: + subfields[current_key] +=3D ' ' + line + i +=3D 1 + + return subfields, i + + def _parse_signal_item(self, lines, i): + """Parse a single signal specification""" + signal =3D {'name': lines[i].strip()} + subfields, next_i =3D self._parse_subfields(lines, i) + + # `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 =3D subfields.get('direction', 'KAPI_SIGNAL_RECEIVE') + raw_action =3D subfields.get('action', 'KAPI_SIGNAL_ACTION_R= ETURN') + raw_timing =3D 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_DI= R_ALIASES), + 'action': _canon_token(raw_action, _SIGNAL_ACTION_ALIAS= ES), + 'condition': subfields.get('condition'), + 'desc': subfields.get('desc'), + 'error': subfields.get('errno'), + 'timing': _canon_bitmask_expr(raw_timing, _SIGNAL_TIMIN= G_ALIASES) + if raw_timing else None, + 'priority': subfields.get('priority'), + 'restartable': subfields.get('restartable', '').lower() =3D= =3D 'yes', + 'interruptible': subfields.get('interruptible', '').lower() = =3D=3D '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('queu= e_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 =3D 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 =3D {'line': line, 'desc': ''} + + # Look for desc: continuation + i +=3D 1 + desc_lines =3D [] + while i < len(lines): + next_line =3D lines[i].strip() + if next_line.startswith('desc:'): + desc_lines.append(next_line[5:].strip()) + i +=3D 1 + elif not next_line: + break + elif not desc_lines and re.match(r'^[A-Z][A-Z0-9_]+,', next_li= ne): + # New error entry, but only if we haven't started a desc b= lock + break + else: + desc_lines.append(next_line) + i +=3D 1 + + if desc_lines: + error['desc'] =3D ' '.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 =3D lines[i].strip() + if not head: + return None, i + 1 + + parts =3D head.split(',', 1) + subfields, next_i =3D self._parse_subfields(lines, i) + + name =3D parts[0].strip() + type_raw =3D (parts[1].strip() if len(parts) >=3D 2 + else subfields.get('type', '').strip()) + if not name or not type_raw: + return None, next_i + + lock =3D { + 'name': name, + 'type': _canon_token(type_raw, _LOCK_TYPE_ALIASES), + } + + for field in ['acquired', 'released', 'held-on-entry', 'held-on-ex= it']: + if subfields.get(field, '').lower() in ('true', 'yes'): + lock[field] =3D True + + lock['desc'] =3D subfields.get('desc', '') + + return lock, next_i + + def _parse_constraint_item(self, lines, i): + """Parse a single constraint specification""" + line =3D lines[i].strip() + + # Check for old format with comma + if ',' in line: + parts =3D line.split(',', 1) + constraint =3D { + 'name': parts[0].strip(), + 'desc': parts[1].strip() if len(parts) > 1 else '', + 'expr': None + } + else: + constraint =3D {'name': line, 'desc': '', 'expr': None} + + subfields, next_i =3D self._parse_subfields(lines, i) + + if 'desc' in subfields: + constraint['desc'] =3D (constraint['desc'] + ' ' + subfields['= desc']).strip() + constraint['expr'] =3D subfields.get('expr') + + return constraint, next_i + + def _parse_side_effect_item(self, lines, i): + """Parse a single side effect specification""" + line =3D lines[i].strip() + + # Default to new format + effect =3D { + 'type': line, + 'target': '', + 'desc': '', + 'condition': None, + 'reversible': False + } + + # Check for old format with commas + if ',' in line: + # Handle condition and reversible flags + cond_match =3D re.search(r',\s*condition=3D([^,]+?)(?:\s*,\s*r= eversible=3D(yes|no)\s*)?$', line) + if cond_match: + effect['condition'] =3D cond_match.group(1).strip() + effect['reversible'] =3D cond_match.group(2) =3D=3D 'yes' + line =3D line[:cond_match.start()] + elif ', reversible=3Dyes' in line: + effect['reversible'] =3D True + line =3D line.replace(', reversible=3Dyes', '') + elif ', reversible=3Dno' in line: + line =3D line.replace(', reversible=3Dno', '') + + parts =3D line.split(',', 2) + if len(parts) >=3D 1: + effect['type'] =3D parts[0].strip() + if len(parts) >=3D 2: + effect['target'] =3D parts[1].strip() + if len(parts) >=3D 3: + effect['desc'] =3D parts[2].strip() + else: + # Multi-line format with subfields + subfields, next_i =3D self._parse_subfields(lines, i) + effect.update({ + 'target': subfields.get('target', ''), + 'desc': subfields.get('desc', ''), + 'condition': subfields.get('condition'), + 'reversible': subfields.get('reversible', '').lower() =3D= =3D 'yes' + }) + return effect, next_i + + return effect, i + 1 + + def _parse_state_trans_item(self, lines, i): + """Parse a single state transition specification""" + line =3D lines[i].strip() + + trans =3D { + 'target': line, + 'from': '', + 'to': '', + 'condition': '', + 'desc': '' + } + + # Check for old format with commas + if ',' in line: + parts =3D line.split(',', 3) + if len(parts) >=3D 1: + trans['target'] =3D parts[0].strip() + if len(parts) >=3D 2: + trans['from'] =3D parts[1].strip() + if len(parts) >=3D 3: + trans['to'] =3D parts[2].strip() + if len(parts) >=3D 4: + desc_part =3D parts[3].strip() + desc_parts =3D desc_part.split(',', 1) + if len(desc_parts) > 1: + trans['condition'] =3D desc_parts[0].strip() + trans['desc'] =3D desc_parts[1].strip() + else: + trans['desc'] =3D desc_part + return trans, i + 1 + else: + # Multi-line format with subfields + subfields, next_i =3D self._parse_subfields(lines, i) + trans.update({ + '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 =3D len(parameterlist) + if param_count > 0: + self.data +=3D f"\n\tKAPI_PARAM_COUNT({param_count})\n" + + for param_idx, param in enumerate(parameterlist): + param_name =3D param.strip() + param_desc =3D parameterdescs.get(param_name, '') + param_ctype =3D parametertypes.get(param_name, '') + + # Parse parameter specifications + param_section =3D self._get_raw_section(sections, 'param') + param_specs =3D {} + if param_section: + param_specs =3D self._parse_param_spec(param_section, para= m_name) + + self.data +=3D f"\n\tKAPI_PARAM({param_idx}, {self._format_mac= ro_param(param_name)}, " + self.data +=3D f"{self._format_macro_param(param_ctype)}, {sel= f._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 +=3D f"\t\t{macro}({param_specs[key]})\n" + + # Handle constraint type + if 'param-constraint-type' in param_specs: + ctype =3D param_specs['param-constraint-type'] + if ctype =3D=3D 'KAPI_CONSTRAINT_BITMASK': + ctype =3D 'KAPI_CONSTRAINT_MASK' + self.data +=3D 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 =3D param_specs['param-range'].split(',',= 1) + self.data +=3D 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 =3D (...)` initializer so we can wrap + # arch-specific bits in #ifdef CONFIG_ ... #endif =E2=80= =94 + # which is illegal inside a function-like macro argument. + if 'param-mask' in param_specs: + arch_masks =3D param_specs.get('param-arch-mask') or [] + if arch_masks: + base =3D param_specs['param-mask'] + self.data +=3D f"\t\t.valid_mask =3D ({base})" + for arch, bits in arch_masks: + config =3D _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 +=3D (f"\n#ifdef {config}\n" + f"\t\t\t| ({bits})\n" + f"#endif\n") + self.data +=3D "\t\t,\n" + else: + self.data +=3D ( + f"\t\tKAPI_PARAM_VALID_MASK(" + f"{param_specs['param-mask']})\n") + + # Handle enum values + if 'param-enum-values' in param_specs: + self.data +=3D 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 +=3D f"\t\tKAPI_PARAM_SIZE_PARAM({param_specs['p= aram-size-param']})\n" + + # Handle constraint description + if 'param-constraint' in param_specs: + self.data +=3D f"\t\tKAPI_PARAM_CONSTRAINT({self._format_m= acro_param(param_specs['param-constraint'])})\n" + + self.data +=3D "\t},\n" + + def _parse_param_spec(self, section_content, param_name): + """Parse parameter specifications from indented format""" + specs =3D {} + lines =3D section_content.strip().split('\n') + current_item =3D None + + # Map to expected keys + field_map =3D { + '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 =3D 0 + while i < len(lines): + line =3D lines[i] + if not line.strip(): + i +=3D 1 + continue + + # Check if this is our parameter (non-indented line) + if not line.startswith((' ', '\t')): + parts =3D line.strip().split(',', 1) + current_item =3D param_name if parts[0].strip() =3D=3D par= am_name else None + if current_item and len(parts) > 1: + specs['param-type'] =3D parts[1].strip() + i +=3D 1 + elif current_item =3D=3D param_name: + # Parse subfield + stripped =3D line.strip() + if ':' in stripped: + key, value =3D stripped.split(':', 1) + key =3D key.strip() + value =3D value.strip() + + # Collect continuation lines (indented lines without a= colon that + # defines a new key, i.e., lines that are pure continu= ations) + i +=3D 1 + while i < len(lines): + next_line =3D 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 =3D next_line.strip() + # Stop if we hit a new key (contains colon with kn= own key prefix) + if next_stripped and ':' in next_stripped: + potential_key =3D 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 =3D value + ' ' + next_stripped + i +=3D 1 + + if key in field_map: + # Clean up the value - remove excessive whitespace + value =3D ' '.join(value.split()) + mapped =3D field_map[key] + if mapped =3D=3D 'param-type': + # Single token sets the type; additional + # comma-separated tokens are flags OR'd + # into param-flags. + ty, extra_flags =3D _split_type_line(value) + if ty: + specs['param-type'] =3D ty + if extra_flags: + existing =3D specs.get('param-flags', '') + merged =3D (existing + ' | ' if existing e= lse '') \ + + ' | '.join(extra_flags) + specs['param-flags'] =3D merged + elif mapped =3D=3D 'param-flags': + specs['param-flags'] =3D _canon_flags_expr(val= ue) + elif mapped =3D=3D '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 =3D _split_constraint_expr(value) + if parsed is not None: + ctype, extras =3D parsed + specs['param-constraint-type'] =3D ctype + for aux_k, aux_v in extras.items(): + specs[aux_k] =3D aux_v + else: + specs['param-constraint-type'] =3D value + elif mapped =3D=3D 'param-arch-mask': + # `arch-mask: =3D ` =E2=80=94 mul= tiple + # entries accumulate into a list of + # (arch, bits) tuples that the emitter + # turns into per-arch #ifdef-guarded mask + # contributions. + if '=3D' in value: + arch, bits =3D value.split('=3D', 1) + specs.setdefault(mapped, []).append( + (arch.strip(), bits.strip())) + else: + specs[mapped] =3D value + else: + i +=3D 1 + + return specs + + def _validate_effect_type(self, effect_type): + """Validate and normalize effect type""" + if 'KAPI_EFFECT_SCHEDULER' in effect_type: + return effect_type.replace('KAPI_EFFECT_SCHEDULER', 'KAPI_EFFE= CT_SCHEDULE') + + if 'KAPI_EFFECT_' in effect_type and effect_type not in VALID_EFFE= CT_TYPES: + if '|' in effect_type: + parts =3D [p.strip() for p in effect_type.split('|')] + valid_parts =3D [] + for p in parts: + if p in VALID_EFFECT_TYPES: + valid_parts.append(p) + else: + import sys + print(f"warning: unrecognized effect type '{p}', " + f"defaulting to KAPI_EFFECT_MODIFY_STATE", f= ile=3Dsys.stderr) + valid_parts.append('KAPI_EFFECT_MODIFY_STATE') + return ' | '.join(valid_parts) + import sys + print(f"warning: unrecognized effect type '{effect_type}', " + f"defaulting to KAPI_EFFECT_MODIFY_STATE", file=3Dsys.st= derr) + 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 at least 2 KAPI-specific section indicators are pr= esent. + We require 2+ indicators (not just 1) to avoid false positives from + regular kernel-doc comments that happen to use a common section na= me + like 'return' or 'error'. Having multiple KAPI sections strongly + suggests intentional API specification rather than coincidence. + """ + indicators =3D [ + 'api-type', 'context-flags', 'contexts', + 'param-type', 'error-code', + 'capability', 'signal', 'lock', 'state-trans', 'constraint', + 'side-effect', 'long-desc' + ] + + count =3D sum(1 for ind in indicators + if any(key.lower().startswith(ind.lower()) or + key.lower().startswith('@' + ind.lower()) + for key in sections.keys())) + + # Require 2+ indicators to distinguish from regular kernel-doc + return count >=3D 2 + + def out_function(self, fname, name, args): + """Generate API spec for a function""" + function_name =3D args.get('function', name) + sections =3D args.sections if hasattr(args, 'sections') else args.= get('sections', {}) + + if not self._has_api_spec(sections): + return + + parameterlist =3D args.parameterlist if hasattr(args, 'parameterli= st') else args.get('parameterlist', []) + parameterdescs =3D args.parameterdescs if hasattr(args, 'parameter= descs') else args.get('parameterdescs', {}) + parametertypes =3D args.parametertypes if hasattr(args, 'parameter= types') else args.get('parametertypes', {}) + purpose =3D args.get('purpose', '') + + # Start macro invocation + self.data +=3D f"DEFINE_KERNEL_API_SPEC({function_name})\n" + + # Basic info + if purpose: + self.data +=3D f"\tKAPI_DESCRIPTION({self._format_macro_param(= purpose)})\n" + + long_desc =3D self._get_multiline_section(sections, 'long-desc') + if long_desc: + # The kernel stores long_description as a pointer into .rodata, + # not a fixed-size buffer, so we do not truncate here. + self.data +=3D f"\tKAPI_LONG_DESC({self._format_macro_param(lo= ng_desc, 0)})\n" + + # Context flags. `contexts:`, `context-flags:`, and `context:` + # all work; tokens canonicalise to KAPI_CTX_* for KAPI_CONTEXT(). + context =3D (self._get_section(sections, 'contexts') + or self._get_section(sections, 'context-flags') + or self._get_section(sections, 'context')) + if context: + self.data +=3D 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 =3D self._parse_indented_items( + self._get_raw_section(sections, 'error'), + self._parse_error_item + ) + + if errors: + self.data +=3D f"\n\tKAPI_ERROR_COUNT({len(errors)})\n" + + for idx, error in enumerate(errors): + self._output_error(idx, error) + + # Process signals + signals =3D self._parse_indented_items( + self._get_raw_section(sections, 'signal'), + self._parse_signal_item + ) + + if signals: + self.data +=3D 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. Like long_description, these are stored= as + # pointers to .rodata on the kernel side, so no truncation is need= ed. + for key, macro in [ + ('examples', 'KAPI_EXAMPLES'), + ('notes', 'KAPI_NOTES'), + ]: + value =3D self._get_multiline_section(sections, key) + if value: + self.data +=3D f"\n\t{macro}({self._format_macro_param(val= ue, 0)})\n" + + self.data +=3D "\n};\n\n" + + def _process_return(self, sections): + """Process the return value specification from kerneldoc annotatio= ns""" + raw =3D self._get_raw_section(sections, 'return') + if not raw: + return + + # Parse subfields from the return section, handling continuation l= ines + lines =3D raw.strip().split('\n') + subfields =3D {} + current_key =3D None + for line in lines: + stripped =3D line.strip() + if ':' in stripped and not line.startswith((' ', '\t')): + key, value =3D stripped.split(':', 1) + current_key =3D key.strip() + subfields[current_key] =3D value.strip() + elif current_key and stripped: + # Continuation line + subfields[current_key] +=3D ' ' + stripped + + ret_type =3D subfields.get('type', '') + check_type =3D subfields.get('check-type', '') + desc =3D subfields.get('desc', '') + success =3D 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 + if ret_type: + ret_type =3D _canon_token(ret_type, _TYPE_ALIASES) + if check_type: + check_type =3D _canon_token(check_type, _RETURN_CHECK_ALIASES) + + self.data +=3D f"\n\tKAPI_RETURN({self._format_macro_param(ret_typ= e)}, " + self.data +=3D f"{self._format_macro_param(desc)})\n" + + if ret_type: + self.data +=3D f"\t\tKAPI_RETURN_TYPE({ret_type})\n" + + if check_type: + self.data +=3D f"\t\tKAPI_RETURN_CHECK_TYPE({check_type})\n" + + if success and check_type =3D=3D 'KAPI_RETURN_RANGE': + self.data +=3D f"\t\tKAPI_RETURN_SUCCESS_RANGE(0, S64_MAX)\n" + + self.data +=3D "\t},\n" + + def _output_error(self, idx, error): + """Output a single error specification""" + line =3D error['line'] + if line.startswith('-'): + line =3D line[1:].strip() + + parts =3D line.split(',', 2) + if len(parts) =3D=3D 2: + # Format: NAME, description + name =3D parts[0].strip() + short_desc =3D parts[1].strip() + code =3D f"-{name}" + elif len(parts) >=3D 3: + # Format: code, name, description + code =3D parts[0].strip() + name =3D parts[1].strip() + short_desc =3D parts[2].strip() + if not code.startswith('-'): + code =3D f"-{code}" + else: + return + + long_desc =3D error.get('desc', '') or short_desc + + self.data +=3D f"\n\tKAPI_ERROR({idx}, {code}, {self._format_macro= _param(name)}, " + self.data +=3D f"{self._format_macro_param(short_desc)},\n\t\t {= self._format_macro_param(long_desc)})\n" + + def _output_signal(self, idx, signal): + """Output a single signal specification""" + self.data +=3D f"\n\tKAPI_SIGNAL({idx}, {signal['number']}, " + self.data +=3D f"{self._format_macro_param(signal['name'], KAPI_MA= X_SIGNAL_NAME_LEN)}, " + self.data +=3D f"{signal['direction']}, {signal['action']})\n" + + # String-valued subfields emitted as KAPI_SIGNAL_* macros. + if signal.get('condition'): + self.data +=3D f"\t\tKAPI_SIGNAL_CONDITION({self._format_macro= _param(signal['condition'])})\n" + if signal.get('desc'): + self.data +=3D f"\t\tKAPI_SIGNAL_DESC({self._format_macro_para= m(signal['desc'])})\n" + if signal.get('error'): + # KAPI_SIGNAL_ERROR expects a numeric/token expression + # (e.g. -EINTR), not a quoted string. + self.data +=3D f"\t\tKAPI_SIGNAL_ERROR({signal['error']})\n" + + # Enum-valued subfields emitted as unquoted tokens. + if signal.get('timing'): + self.data +=3D f"\t\tKAPI_SIGNAL_TIMING({signal['timing']})\n" + if signal.get('priority'): + self.data +=3D f"\t\tKAPI_SIGNAL_PRIORITY({signal['priority']}= )\n" + + # Boolean flag subfields. + if signal.get('restartable'): + self.data +=3D "\t\tKAPI_SIGNAL_RESTARTABLE\n" + if signal.get('interruptible'): + self.data +=3D "\t\tKAPI_SIGNAL_INTERRUPTIBLE\n" + + # Additional struct fields. Emitted only when present in the + # kerneldoc so existing specs keep producing identical output. + if signal.get('target'): + self.data +=3D f"\t\tKAPI_SIGNAL_TARGET({self._format_macro_pa= ram(signal['target'])})\n" + if signal.get('queue'): + self.data +=3D f"\t\tKAPI_SIGNAL_QUEUE({self._format_macro_par= am(signal['queue'])})\n" + if signal.get('transform'): + # Numeric/token expression (e.g. SIGKILL), not a quoted string. + self.data +=3D f"\t\tKAPI_SIGNAL_TRANSFORM({signal['transform'= ]})\n" + if signal.get('sa_flags_required'): + self.data +=3D f"\t\tKAPI_SIGNAL_SA_FLAGS_REQ({signal['sa_flag= s_required']})\n" + if signal.get('sa_flags_forbidden'): + self.data +=3D f"\t\tKAPI_SIGNAL_SA_FLAGS_FORBID({signal['sa_f= lags_forbidden']})\n" + if signal.get('state_required'): + self.data +=3D f"\t\tKAPI_SIGNAL_STATE_REQ({signal['state_requ= ired']})\n" + if signal.get('state_forbidden'): + self.data +=3D f"\t\tKAPI_SIGNAL_STATE_FORBID({signal['state_f= orbidden']})\n" + + self.data +=3D "\t},\n" + + def _process_locks(self, sections): + """Process lock specifications""" + locks =3D self._parse_indented_items( + self._get_raw_section(sections, 'lock'), + self._parse_lock_item + ) + + if locks: + self.data +=3D f"\n\tKAPI_LOCK_COUNT({len(locks)})\n" + + for idx, lock in enumerate(locks): + self.data +=3D 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=3Doverride-init` at W=3D1. + acquired =3D bool(lock.get('acquired')) + released =3D bool(lock.get('released')) + if acquired and not released: + self.data +=3D "\t\tKAPI_LOCK_ACQUIRED\n" + elif released and not acquired: + self.data +=3D "\t\tKAPI_LOCK_RELEASED\n" + + if lock.get('desc'): + self.data +=3D f"\t\tKAPI_LOCK_DESC({self._format_macr= o_param(lock['desc'])})\n" + + self.data +=3D "\t},\n" + + def _process_constraints(self, sections): + """Process constraint specifications""" + constraints =3D self._parse_indented_items( + self._get_raw_section(sections, 'constraint'), + self._parse_constraint_item + ) + + if constraints: + self.data +=3D f"\n\tKAPI_CONSTRAINT_COUNT({len(constraints)})= \n" + + for idx, constraint in enumerate(constraints): + self.data +=3D f"\n\tKAPI_CONSTRAINT({idx}, {self._format_= macro_param(constraint['name'])},\n" + self.data +=3D f"\t\t\t{self._format_macro_param(constrain= t['desc'])})\n" + + if constraint.get('expr'): + self.data +=3D f"\t\tKAPI_CONSTRAINT_EXPR({self._forma= t_macro_param(constraint['expr'])})\n" + + self.data +=3D "\t},\n" + + def _process_side_effects(self, sections): + """Process side effect specifications""" + effects =3D self._parse_indented_items( + self._get_raw_section(sections, 'side-effect'), + self._parse_side_effect_item + ) + + if effects: + self.data +=3D f"\n\tKAPI_SIDE_EFFECT_COUNT({len(effects)})\n" + + for idx, effect in enumerate(effects): + # Canonicalise aliases (alloc_memory, modify_state, =E2=80= =A6) + # to KAPI_EFFECT_*. Accepts '|' or ',' as separators. + effect_type =3D _canon_bitmask_expr(effect['type'], _EFFEC= T_ALIASES) + effect_type =3D self._validate_effect_type(effect_type) + + self.data +=3D f"\n\tKAPI_SIDE_EFFECT({idx}, {effect_type}= ,\n" + self.data +=3D f"\t\t\t {self._format_macro_param(effect['= target'])},\n" + self.data +=3D f"\t\t\t {self._format_macro_param(effect['= desc'])})\n" + + if effect.get('condition'): + self.data +=3D f"\t\tKAPI_EFFECT_CONDITION({self._form= at_macro_param(effect['condition'])})\n" + + if effect.get('reversible'): + self.data +=3D "\t\tKAPI_EFFECT_REVERSIBLE\n" + + self.data +=3D "\t},\n" + + def _process_state_transitions(self, sections): + """Process state transition specifications""" + transitions =3D self._parse_indented_items( + self._get_raw_section(sections, 'state-trans'), + self._parse_state_trans_item + ) + + if transitions: + self.data +=3D f"\n\tKAPI_STATE_TRANS_COUNT({len(transitions)}= )\n" + + for idx, trans in enumerate(transitions): + desc =3D trans['desc'] + if trans.get('condition'): + desc =3D trans['condition'] + (', ' + desc if desc els= e '') + + self.data +=3D f"\n\tKAPI_STATE_TRANS({idx}, {self._format= _macro_param(trans['target'])}, " + self.data +=3D f"{self._format_macro_param(trans['from'])}= , {self._format_macro_param(trans['to'])},\n" + self.data +=3D f"\t\t\t {self._format_macro_param(desc)})\= n" + self.data +=3D "\t},\n" + + def _process_capabilities(self, sections): + """Process capability specifications""" + cap_section =3D self._get_raw_section(sections, 'capability') + if not cap_section: + return + + lines =3D cap_section.strip().split('\n') + capabilities =3D [] + i =3D 0 + + while i < len(lines): + line =3D lines[i].strip() + # Skip empty lines and subfield lines (they'll be parsed with = their parent) + if not line or line.startswith(('allows:', 'without:', 'condit= ion:', 'priority:', 'type:', 'desc:')): + i +=3D 1 + continue + + cap_info =3D {'line': line} + + # Parse subfields + subfields, next_i =3D self._parse_subfields(lines, i) + cap_info.update(subfields) + capabilities.append(cap_info) + i =3D next_i + + if capabilities: + # Filter out "none" capabilities (no capability required) + valid_caps =3D [cap for cap in capabilities if cap['line'].str= ip().lower() !=3D 'none'] + + if not valid_caps: + return + + self.data +=3D f"\n\tKAPI_CAPABILITY_COUNT({len(valid_caps)})\= n" + + for idx, cap in enumerate(valid_caps): + line =3D cap['line'] + parts =3D line.split(',', 2) + + # Handle both formats: + # 1. New format: "CAP_NAME" with type/desc as subfields + # 2. Old format: "CAP_NAME, TYPE, description" + if len(parts) >=3D 2: + # Old comma-separated format + cap_name =3D parts[0].strip() + cap_type =3D parts[1].strip() + cap_desc =3D parts[2].strip() if len(parts) > 2 else c= ap.get('desc', cap_name) + else: + # New subfield format - capability name on main line + cap_name =3D line.strip() + cap_type =3D cap.get('type', 'KAPI_CAP_PERFORM_OPERATI= ON') + cap_desc =3D cap.get('desc', cap_name) + + # Map capability type aliases to KAPI_CAP_* enum values. + cap_type_map =3D { + 'KAPI_CAP_REQUIRED': 'KAPI_CAP_PERFORM_OPERATION', + '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 =3D cap_type_map.get(cap_type, cap_type) + + # Fix common type issues + if 'BYPASS' in cap_type and cap_type !=3D 'KAPI_CAP_BYPASS= _CHECK': + cap_type =3D 'KAPI_CAP_BYPASS_CHECK' + + # Ensure cap_type is a valid enum + valid_types =3D [ + 'KAPI_CAP_BYPASS_CHECK', 'KAPI_CAP_INCREASE_LIMIT', + 'KAPI_CAP_OVERRIDE_RESTRICTION', 'KAPI_CAP_GRANT_PERMI= SSION', + 'KAPI_CAP_MODIFY_BEHAVIOR', 'KAPI_CAP_ACCESS_RESOURCE', + 'KAPI_CAP_PERFORM_OPERATION' + ] + if cap_type not in valid_types: + cap_type =3D 'KAPI_CAP_PERFORM_OPERATION' + + self.data +=3D f"\n\tKAPI_CAPABILITY({idx}, {cap_name}, {s= elf._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 =3D self._format_macro_param(cap[key]) if ke= y !=3D 'priority' else cap[key] + self.data +=3D f"\t\t{macro}({value})\n" + + self.data +=3D "\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_output.py b/tools/lib/python/kdoc/k= doc_output.py index 4210b91dde5f1..cd91a4f59f275 100644 --- a/tools/lib/python/kdoc/kdoc_output.py +++ b/tools/lib/python/kdoc/kdoc_output.py @@ -129,8 +129,13 @@ class OutputFormat: Output warnings for identifiers that will be displayed. """ =20 - for log_msg in args.warnings: - self.config.warning(log_msg) + warnings =3D getattr(args, 'warnings', []) + + for log_msg in warnings: + # Skip numeric warnings (line numbers) which are false positiv= es + # from parameter-specific sections like "param-constraint: nam= e, value" + if not isinstance(log_msg, int): + self.config.warning(log_msg) =20 def check_doc(self, name, args): """Check if DOC should be output.""" diff --git a/tools/lib/python/kdoc/kdoc_parser.py b/tools/lib/python/kdoc/k= doc_parser.py index ca00695b47b31..daf17f535b1ee 100644 --- a/tools/lib/python/kdoc/kdoc_parser.py +++ b/tools/lib/python/kdoc/kdoc_parser.py @@ -28,6 +28,23 @@ from kdoc.kdoc_item import KdocItem # Allow whitespace at end of comment start. doc_start =3D KernRe(r'^/\*\*\s*$', cache=3DFalse) =20 +# Sections that are allowed to be duplicated for API specifications +# These represent lists of items (multiple errors, signals, etc.) +ALLOWED_DUPLICATE_SECTIONS =3D { + '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 =3D KernRe(r'\*/', cache=3DFalse) doc_com =3D KernRe(r'\s*\*\s*', cache=3DFalse) doc_com_body =3D KernRe(r'\s*\* ?', cache=3DFalse) @@ -40,10 +57,71 @@ doc_decl =3D doc_com + KernRe(r'(\w+)', cache=3DFalse) # @{section-name}: # while trying to not match literal block starts like "example::" # +# Base kernel-doc section names known_section_names =3D 'description|context|returns?|notes?|examples?' -known_sections =3D KernRe(known_section_names, flags =3D re.I) + +# API specification section names (for KAPI spec framework) +# Format: (base_name, has_count_variant, has_other_variants) +# Sections with has_count_variant=3DTrue need negative lookahead in doc_se= ct +# to avoid matching 'error' when 'error-count' is intended +_kapi_base_sections =3D [ + # (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 =3D [] # For known_sections (simple validation) + parsing_parts =3D [] # For 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 =3D _build_kapi_patterns() + +known_sections =3D KernRe(known_section_names + '|' + _kapi_validation_pat= tern, + flags=3Dre.I) doc_sect =3D doc_com + \ - KernRe(r'\s*(@[.\w]+|@\.\.\.|' + known_section_names + r')\s*:([^:].*)= ?$', + KernRe(r'\s*(@[.\w\-]+|@\.\.\.|' + known_section_names + '|' + + _kapi_parsing_pattern + r')\s*:([^:].*)?$', flags=3Dre.I, cache=3DFalse) =20 doc_content =3D doc_com_body + KernRe(r'(.*)', cache=3DFalse) @@ -349,7 +427,9 @@ class KernelEntry: else: if name in self.sections and self.sections[name] !=3D "": # Only warn on user-specified duplicate section names - if name !=3D SECTION_DEFAULT: + # Skip warning for sections that are expected to have dupl= icates + # (like error, param, signal, etc. for API specifications) + if name !=3D SECTION_DEFAULT and name not in ALLOWED_DUPLI= CATE_SECTIONS: self.emit_msg(self.new_start_line, f"duplicate section name '{name}'") # Treat as a new paragraph - add a blank line --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id DC8EA3A960E; Fri, 29 May 2026 23:33:24 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097607; cv=none; b=GvYs5XwnbHoZxb59dc0cHJjkZzW2vrvaaCQNgTz+NPwWTOM/LvOZ5HF0GacCFK8awImQJ2iB5sz4GW7YwgHF9I/EycZ7ZzQ7bQNW1fplahFrR1ieJaAMAfgQVhB1NQ2VP/ceIMz3xrZMlNoplFQyGr8F0jpu+1zqUexrdtZs5DE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097607; c=relaxed/simple; bh=/UoUnRlu7ctqjV2qqqC8PyG2pzw9oLcE1R0HqG4IlB8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=ChvuL8Z9kqfzUjkzbzTARAkQeUjbLVi/JS+UoWWfWzQbQLK9CpvBLqy33/rQlJCHuwckklwmnDLhz8gohB38/z8rn4AV8BIPKr4AiHb58hIz5RuocPEjj7zggH7cIbEMW4tGMpx+m6cav/2TgN6c36/yYxICp6ElSPtz0+8Mx8A= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=bNyZqNrg; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="bNyZqNrg" Received: by smtp.kernel.org (Postfix) with ESMTPSA id B73401F0089A; Fri, 29 May 2026 23:33:21 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097604; bh=DKBz4UUQi5FZnp0KQg62LGM0RNH+YNYQirs5JJJrbPc=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=bNyZqNrgcQXM3hbo6w0ybYwWeDJc246y7hWPcBGBKt6ktWQcgad0X+0iojj0lJ/m1 E9mD7TzPnc9/g0nj/fyB8Db7aZ8WsmsnZoBYIyvQDJfiBow0roQLDbC+pVTUB02A+L 682OTNdyz6T83DnBn0cSMmytB/vklgjSg376SPBltrk6F81uK3jMc664ubhCFaJdhP NGyMWIXezXXouzvM1baiyd38HB1BhawSsSUIWgzZOORWYaPmBYfxUQowZ+4L3ecVku MoA2dK44IZezczZ14ewpUUVygqjNO+09D+crQJBUad03L77TJ7Znpx5sCFDxkpOyFM XLpy61DQLY8MA== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 03/11] kernel/api: add debugfs interface for kernel API specifications Date: Fri, 29 May 2026 19:33:02 -0400 Message-ID: <20260529233311.1901670-4-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add a debugfs interface to expose kernel API specifications at runtime. This allows tools and users to query the complete API specifications through the debugfs filesystem. The interface provides: - /sys/kernel/debug/kapi/list - lists all available API specifications - /sys/kernel/debug/kapi/specs/ - detailed info for each API Each specification file includes: - Function name, version, and descriptions - Execution context requirements and flags - Parameter details with types, flags, and constraints - Return value specifications and success conditions - Error codes with descriptions and conditions - Locking requirements and constraints - Signal handling specifications - Examples, notes, and deprecation status This enables runtime introspection of kernel APIs for documentation tools, static analyzers, and debugging purposes. Signed-off-by: Sasha Levin --- Documentation/dev-tools/kernel-api-spec.rst | 49 +- kernel/api/kapi_debugfs.c | 553 ++++++++++++++++++++ 2 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 kernel/api/kapi_debugfs.c diff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/de= v-tools/kernel-api-spec.rst index 62a1248746622..27da0f12ea3fc 100644 --- a/Documentation/dev-tools/kernel-api-spec.rst +++ b/Documentation/dev-tools/kernel-api-spec.rst @@ -242,6 +242,7 @@ Runtime validation is controlled by kernel configuratio= n: =20 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 =20 Validation Behavior ------------------- @@ -282,6 +283,51 @@ custom validation functions via the ``validate`` field= in the constraint spec: .type =3D KAPI_CONSTRAINT_CUSTOM, .validate =3D validate_buffer_size, =20 +DebugFS Interface +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +The debugfs interface provides runtime access to API specifications: + +Directory Structure +------------------- + +:: + + /sys/kernel/debug/kapi/ + =E2=94=9C=E2=94=80=E2=94=80 list # Overview of all= registered API specs + =E2=94=94=E2=94=80=E2=94=80 specs/ # Per-API specifi= cation files + =E2=94=9C=E2=94=80=E2=94=80 sys_open # Human-readable = spec for sys_open + =E2=94=9C=E2=94=80=E2=94=80 sys_close # Human-readable = spec for sys_close + =E2=94=9C=E2=94=80=E2=94=80 sys_read # Human-readable = spec for sys_read + =E2=94=94=E2=94=80=E2=94=80 sys_write # Human-readable = spec for sys_write + +Usage Examples +-------------- + +List all available API specifications:: + + $ cat /sys/kernel/debug/kapi/list + Available Kernel API Specifications + =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + + 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 + + Total: 4 specifications + +Query specific API:: + + $ cat /sys/kernel/debug/kapi/specs/sys_open + Kernel API Specification + =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D + + Name: sys_open + Version: 1 + Description: Open or create a file + ... + Performance Considerations =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D =20 @@ -654,7 +700,8 @@ Submitting Specifications 1. Add specifications to the same file as the API implementation 2. Follow existing patterns and naming conventions 3. Test with CONFIG_KAPI_RUNTIME_CHECKS enabled -4. Run scripts/checkpatch.pl on your changes +4. Verify debugfs output is correct +5. Run scripts/checkpatch.pl on your changes =20 Review Criteria --------------- diff --git a/kernel/api/kapi_debugfs.c b/kernel/api/kapi_debugfs.c new file mode 100644 index 0000000000000..5a78287fc6cb8 --- /dev/null +++ b/kernel/api/kapi_debugfs.c @@ -0,0 +1,553 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026 Sasha Levin + * + * 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 +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +static struct dentry *kapi_debugfs_root; + +/* Helper function to print parameter type as string */ +static const char * const param_type_names[] =3D { + [KAPI_TYPE_VOID] =3D "void", + [KAPI_TYPE_INT] =3D "int", + [KAPI_TYPE_UINT] =3D "uint", + [KAPI_TYPE_PTR] =3D "ptr", + [KAPI_TYPE_STRUCT] =3D "struct", + [KAPI_TYPE_UNION] =3D "union", + [KAPI_TYPE_ARRAY] =3D "array", + [KAPI_TYPE_FD] =3D "fd", + [KAPI_TYPE_ENUM] =3D "enum", + [KAPI_TYPE_USER_PTR] =3D "user_ptr", + [KAPI_TYPE_PATH] =3D "path", + [KAPI_TYPE_FUNC_PTR] =3D "func_ptr", + [KAPI_TYPE_CUSTOM] =3D "custom", +}; + +static const char *param_type_str(enum kapi_param_type type) +{ + if (type < ARRAY_SIZE(param_type_names) && param_type_names[type]) + return param_type_names[type]; + return "unknown"; +} + +/* 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) =3D=3D 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"); +} + +/* Show function for individual API spec */ +static int kapi_spec_show(struct seq_file *m, void *v) +{ + struct kernel_api_spec *spec =3D m->private; + int i; + + seq_puts(m, "Kernel API Specification\n"); + seq_puts(m, "=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D\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_printf(m, "Long description: %s\n", 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 =3D 0; i < spec->param_count && i < KAPI_MAX_PARAMS; i++) { + struct kapi_param_spec *param =3D &spec->params[i]; + + seq_printf(m, " [%d] %s:\n", i, param->name); + seq_printf(m, " type: %s (%s)\n", + param_type_str(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 !=3D KAPI_CONSTRAINT_NONE) { + seq_puts(m, " constraints:\n"); + switch (param->constraint_type) { + 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); + 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); + 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"); + if (param->constraints && *param->constraints) + seq_printf(m, " description: %s\n", + param->constraints); + break; + default: + break; + } + } + 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: =3D=3D %lld\n", spec->return_spec.success_valu= e); + 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 (>=3D 0)\n"); + break; + case KAPI_RETURN_ERROR_CHECK: + seq_puts(m, " success: error check\n"); + break; + case KAPI_RETURN_CUSTOM: + seq_puts(m, " success: custom check\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 =3D 0; i < spec->error_count && i < KAPI_MAX_ERRORS; i++) { + struct kapi_error_spec *err =3D &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 =3D 0; i < spec->lock_count && i < KAPI_MAX_LOCKS; i++) { + struct kapi_lock_spec *lock =3D &spec->locks[i]; + const char *type_str, *scope_str; + + switch (lock->lock_type) { + case KAPI_LOCK_MUTEX: + type_str =3D "mutex"; + break; + case KAPI_LOCK_SPINLOCK: + type_str =3D "spinlock"; + break; + case KAPI_LOCK_RWLOCK: + type_str =3D "rwlock"; + break; + case KAPI_LOCK_SEMAPHORE: + type_str =3D "semaphore"; + break; + case KAPI_LOCK_RCU: + type_str =3D "rcu"; + break; + case KAPI_LOCK_SEQLOCK: + type_str =3D "seqlock"; + break; + default: + type_str =3D "unknown"; + break; + } + switch (lock->scope) { + case KAPI_LOCK_INTERNAL: + scope_str =3D "acquired and released"; + break; + case KAPI_LOCK_ACQUIRES: + scope_str =3D "acquired (not released)"; + break; + case KAPI_LOCK_RELEASES: + scope_str =3D "released (held on entry)"; + break; + case KAPI_LOCK_CALLER_HELD: + scope_str =3D "held by caller"; + break; + default: + scope_str =3D "unknown"; + break; + } + seq_printf(m, " %s (%s): %s\n", + lock->lock_name, type_str, lock->description); + seq_printf(m, " scope: %s\n", scope_str); + } + seq_puts(m, "\n"); + } + + /* Constraints */ + if (spec->constraint_count > 0) { + seq_printf(m, "Additional constraints (%u):\n", spec->constraint_count); + for (i =3D 0; i < spec->constraint_count && i < KAPI_MAX_CONSTRAINTS; i+= +) { + struct kapi_constraint_spec *cons =3D &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 =3D 0; i < spec->signal_count && i < KAPI_MAX_SIGNALS; i++) { + struct kapi_signal_spec *sig =3D &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 =3D 0; i < spec->side_effect_count && i < KAPI_MAX_SIDE_EFFECTS; = i++) { + const struct kapi_side_effect *eff =3D &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 =3D 0; i < spec->state_trans_count && i < KAPI_MAX_STATE_TRANS; i= ++) { + const struct kapi_state_transition *trans =3D &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 =3D 0; i < spec->capability_count && i < KAPI_MAX_CAPABILITIES; i= ++) { + const struct kapi_capability_spec *cap =3D &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_printf(m, "Examples:\n%s\n\n", spec->examples); + if (spec->notes && *spec->notes) + seq_printf(m, "Notes:\n%s\n\n", spec->notes); + + 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 =3D { + .open =3D kapi_spec_open, + .read =3D seq_read, + .llseek =3D seq_lseek, + .release =3D single_release, +}; + +/* + * JSON view of a single spec. Calls kapi_export_json() into a temporary + * buffer and pastes the result into the seq_file. This gives userspace a + * machine-readable rendering that matches the output produced by + * kapi_export_json() from --vmlinux / --source tooling, so consumers can + * diff the three modes for equivalence. + */ +#define KAPI_JSON_BUF_SIZE (64 * 1024) + +static int kapi_spec_json_show(struct seq_file *m, void *v) +{ + const struct kernel_api_spec *spec =3D m->private; + char *buf; + int ret; + + if (!spec) + return -EINVAL; + + buf =3D kmalloc(KAPI_JSON_BUF_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + ret =3D kapi_export_json(spec, buf, KAPI_JSON_BUF_SIZE); + if (ret < 0) { + kfree(buf); + return ret; + } + + seq_puts(m, buf); + kfree(buf); + 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 =3D { + .open =3D kapi_spec_json_open, + .read =3D seq_read, + .llseek =3D seq_lseek, + .release =3D single_release, +}; + +/* + * Show all available API specs. + * + * Note: This only iterates the static .kapi_specs section. Specs register= ed + * 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 =3D 0; + + seq_puts(m, "Available Kernel API Specifications\n"); + seq_puts(m, "=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D\n\n"); + + for (pp =3D __start_kapi_specs; pp < __stop_kapi_specs; pp++) { + const struct kernel_api_spec *spec =3D *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 =3D { + .open =3D kapi_list_open, + .read =3D seq_read, + .llseek =3D seq_lseek, + .release =3D single_release, +}; + +static int __init kapi_debugfs_init(void) +{ + const struct kernel_api_spec * const *pp; + struct dentry *spec_dir, *json_dir; + + kapi_debugfs_root =3D debugfs_create_dir("kapi", NULL); + if (IS_ERR(kapi_debugfs_root)) { + pr_warn("kapi: cannot create /sys/kernel/debug/kapi: %pe\n", + kapi_debugfs_root); + kapi_debugfs_root =3D NULL; + return 0; + } + + debugfs_create_file("list", 0444, kapi_debugfs_root, NULL, + &kapi_list_fops); + spec_dir =3D debugfs_create_dir("specs", kapi_debugfs_root); + json_dir =3D debugfs_create_dir("specs-json", kapi_debugfs_root); + + for (pp =3D __start_kapi_specs; pp < __stop_kapi_specs; pp++) { + const struct kernel_api_spec *spec =3D *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); --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1581F3C5526; Fri, 29 May 2026 23:33:27 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097625; cv=none; b=HYOwxxUUIPqQs05zZO5OoT77ow1Iob6BMmqFpGhZggjl/4Lw9dS3+3yuS0+SW5uIg/TWtpN4uvGp2WFrLq9rfAKVSK5hGViCd9F45+eUKbKaNBioGqMIVz+IUXmeNzEA7gHXABVsYU2KsvJjPXf1TxuIEp7nF4uB03zaBHi+fUo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097625; c=relaxed/simple; bh=ZgqKjnTS7J0ihg5/tDS17UPqHVJQmg8tmR88wlgqBng=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=JK3tKgSB5qxfI0x43Vuj7snerUCDsueMFNbisSnfrSsqMYHk+Fz1thkpsYNl+MwxWE94e7JOmSKR/NWLfuQ37AgaplqQ8+jOtB/yhWVM1JH72bcjZRE4k8IIEzZKj+evQVcC4V0+xhYmML4DTHS6RJh4sfSVvR1sWoC5jvXL604= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=asaF4NKY; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="asaF4NKY" Received: by smtp.kernel.org (Postfix) with ESMTPSA id C61CB1F0089C; Fri, 29 May 2026 23:33:24 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097607; bh=t3vwQnNb0XETJjMUt5UdwmitYQVXcO8HbYnNIxoK5tA=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=asaF4NKYTv3FcSmFIErjc51deB84tmo+I9+xeqqemF5TulD90SmWRj+XFdX5sLKwN /95kJzxAK2vrDmpakAt0kRb56CydBq6smtPv5BYchsnyJT5awZbc7a4EgV6u4S+qAR z/tu7E8WqnTMla6esSDLnK1t/9rWdsrcLxmCAzYJWKXUxdURd26ipUsp9h+5qsFze5 p0K82R7u3vD7pTnDXlXS15e/BtkTFG9Vt/vWBnc1kQt4Dkor6dlvs6VVbj4DPn9MHt H8zsAur1HcECTO/xu0lSYEHh0m+vr9fXQAaa4tNrw1MCzMZExjJDUMgZhcuWHmSZNP tPtAN/WMNDOug== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 04/11] tools/kapi: add kernel API specification extraction tool Date: Fri, 29 May 2026 19:33:03 -0400 Message-ID: <20260529233311.1901670-5-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable The kapi tool extracts and renders kernel API specifications from three input sources and emits them in one of three output formats: Input modes: --source PATH parse kerneldoc blocks from a C source file or directory --vmlinux PATH decode the `.kapi_specs` ELF section from a compiled kernel binary --debugfs PATH read the spec dumps exposed under /sys/kernel/debug/kapi/ on a running kernel Output formats: plain, json, rst The tool is written in Rust and has no runtime dependencies beyond cargo. It ships alongside the kernel to give documentation tools, static analyzers, and IDE integrations a single entry point for querying the spec data produced by the framework. Signed-off-by: Sasha Levin --- Documentation/dev-tools/kernel-api-spec.rst | 15 +- tools/kapi/.gitignore | 4 + tools/kapi/Cargo.lock | 679 ++++ tools/kapi/Cargo.toml | 20 + tools/kapi/Makefile | 33 + tools/kapi/README.md | 32 + tools/kapi/src/extractor/debugfs.rs | 849 +++++ tools/kapi/src/extractor/kerneldoc_parser.rs | 2831 +++++++++++++++++ tools/kapi/src/extractor/mod.rs | 388 +++ tools/kapi/src/extractor/source_parser.rs | 415 +++ .../src/extractor/vmlinux/binary_utils.rs | 462 +++ .../src/extractor/vmlinux/magic_finder.rs | 115 + tools/kapi/src/extractor/vmlinux/mod.rs | 857 +++++ tools/kapi/src/formatter/json.rs | 634 ++++ tools/kapi/src/formatter/mod.rs | 122 + tools/kapi/src/formatter/plain.rs | 646 ++++ tools/kapi/src/formatter/rst.rs | 726 +++++ tools/kapi/src/main.rs | 123 + 18 files changed, 8942 insertions(+), 9 deletions(-) create mode 100644 tools/kapi/.gitignore create mode 100644 tools/kapi/Cargo.lock create mode 100644 tools/kapi/Cargo.toml create mode 100644 tools/kapi/Makefile create mode 100644 tools/kapi/README.md create mode 100644 tools/kapi/src/extractor/debugfs.rs create mode 100644 tools/kapi/src/extractor/kerneldoc_parser.rs create mode 100644 tools/kapi/src/extractor/mod.rs create mode 100644 tools/kapi/src/extractor/source_parser.rs create mode 100644 tools/kapi/src/extractor/vmlinux/binary_utils.rs create mode 100644 tools/kapi/src/extractor/vmlinux/magic_finder.rs create mode 100644 tools/kapi/src/extractor/vmlinux/mod.rs create mode 100644 tools/kapi/src/formatter/json.rs create mode 100644 tools/kapi/src/formatter/mod.rs create mode 100644 tools/kapi/src/formatter/plain.rs create mode 100644 tools/kapi/src/formatter/rst.rs create mode 100644 tools/kapi/src/main.rs diff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/de= v-tools/kernel-api-spec.rst index 27da0f12ea3fc..26598a98c0f69 100644 --- a/Documentation/dev-tools/kernel-api-spec.rst +++ b/Documentation/dev-tools/kernel-api-spec.rst @@ -30,7 +30,9 @@ The framework aims to: common programming errors during development and testing. =20 3. **Support Tooling**: Export API specifications in machine-readable form= ats for - use by static analyzers, documentation generators, and development tool= s. + use by static analyzers, documentation generators, and development tool= s. The + ``kapi`` tool (see `The kapi Tool`_) provides comprehensive extraction = and + formatting capabilities. =20 4. **Formalize Contracts**: Explicitly document API contracts including pa= rameter constraints, execution contexts, locking requirements, and side effects. @@ -551,15 +553,10 @@ Modern IDEs can use the specification data for: - Context validation - Error code documentation =20 -Testing Framework ------------------ - -The framework includes test helpers:: +Example IDE integration:: =20 - #ifdef CONFIG_KAPI_TESTING - /* Verify API behaves according to specification */ - kapi_test_api("kmalloc", test_cases); - #endif + # Generate IDE completion data + $ kapi --format json > .vscode/kernel-apis.json =20 Best Practices =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D diff --git a/tools/kapi/.gitignore b/tools/kapi/.gitignore new file mode 100644 index 0000000000000..1390bfc12686c --- /dev/null +++ b/tools/kapi/.gitignore @@ -0,0 +1,4 @@ +# 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 =3D 4 + +[[package]] +name =3D "aho-corasick" +version =3D "1.1.4" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f100281617= 4301" +dependencies =3D [ + "memchr", +] + +[[package]] +name =3D "anstream" +version =3D "1.0.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815c= a28d" +dependencies =3D [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name =3D "anstyle" +version =3D "1.0.14" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead7= 6000" + +[[package]] +name =3D "anstyle-parse" +version =3D "1.0.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8= 130e" +dependencies =3D [ + "utf8parse", +] + +[[package]] +name =3D "anstyle-query" +version =3D "1.1.5" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83= dadc" +dependencies =3D [ + "windows-sys", +] + +[[package]] +name =3D "anstyle-wincon" +version =3D "3.0.11" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55= 747d" +dependencies =3D [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name =3D "anyhow" +version =3D "1.0.102" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b= 842c" + +[[package]] +name =3D "bitflags" +version =3D "2.11.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c= 78b3" + +[[package]] +name =3D "cfg-if" +version =3D "1.0.4" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef98614261512= 1801" + +[[package]] +name =3D "clap" +version =3D "4.6.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d2842= 8e51" +dependencies =3D [ + "clap_builder", + "clap_derive", +] + +[[package]] +name =3D "clap_builder" +version =3D "4.6.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5= 069f" +dependencies =3D [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name =3D "clap_derive" +version =3D "4.6.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f9= 97a9" +dependencies =3D [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name =3D "clap_lex" +version =3D "1.1.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d= 98f9" + +[[package]] +name =3D "colorchoice" +version =3D "1.0.5" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139a= e570" + +[[package]] +name =3D "equivalent" +version =3D "1.0.2" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe= 5c0f" + +[[package]] +name =3D "errno" +version =3D "0.3.14" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63= efeb" +dependencies =3D [ + "libc", + "windows-sys", +] + +[[package]] +name =3D "fastrand" +version =3D "2.4.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6def= d8d6" + +[[package]] +name =3D "foldhash" +version =3D "0.1.5" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc8= 3af2" + +[[package]] +name =3D "getrandom" +version =3D "0.4.2" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c740= 6555" +dependencies =3D [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name =3D "goblin" +version =3D "0.10.5" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e= 5cd9" +dependencies =3D [ + "log", + "plain", + "scroll", +] + +[[package]] +name =3D "hashbrown" +version =3D "0.15.5" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f53= 38a1" +dependencies =3D [ + "foldhash", +] + +[[package]] +name =3D "hashbrown" +version =3D "0.17.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb= 0f51" + +[[package]] +name =3D "heck" +version =3D "0.5.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe= 55ea" + +[[package]] +name =3D "id-arena" +version =3D "2.3.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0= d954" + +[[package]] +name =3D "indexmap" +version =3D "2.14.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf9= 49d9" +dependencies =3D [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name =3D "is_terminal_polyfill" +version =3D "1.70.2" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a3= 5695" + +[[package]] +name =3D "itoa" +version =3D "1.0.18" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d777768= 5682" + +[[package]] +name =3D "kapi" +version =3D "0.1.0" +dependencies =3D [ + "anyhow", + "clap", + "goblin", + "regex", + "serde", + "serde_json", + "tempfile", + "walkdir", +] + +[[package]] +name =3D "leb128fmt" +version =3D "0.1.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a8= 1db2" + +[[package]] +name =3D "libc" +version =3D "0.2.185" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a= 2b8f" + +[[package]] +name =3D "linux-raw-sys" +version =3D "0.12.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d5= 8a53" + +[[package]] +name =3D "log" +version =3D "0.4.29" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf797875892= 0897" + +[[package]] +name =3D "memchr" +version =3D "2.8.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff= 3f79" + +[[package]] +name =3D "once_cell" +version =3D "1.21.4" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd31= 3b50" + +[[package]] +name =3D "once_cell_polyfill" +version =3D "1.70.2" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451= b4fe" + +[[package]] +name =3D "plain" +version =3D "0.2.3" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faea= c1a6" + +[[package]] +name =3D "prettyplease" +version =3D "0.2.37" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208= f62b" +dependencies =3D [ + "proc-macro2", + "syn", +] + +[[package]] +name =3D "proc-macro2" +version =3D "1.0.106" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb3= 0934" +dependencies =3D [ + "unicode-ident", +] + +[[package]] +name =3D "quote" +version =3D "1.0.45" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259= b924" +dependencies =3D [ + "proc-macro2", +] + +[[package]] +name =3D "r-efi" +version =3D "6.0.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0= d9bf" + +[[package]] +name =3D "regex" +version =3D "1.12.3" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0= e276" +dependencies =3D [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name =3D "regex-automata" +version =3D "0.4.14" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213c= ae8f" +dependencies =3D [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name =3D "regex-syntax" +version =3D "0.8.10" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa01354318= 5d0a" + +[[package]] +name =3D "rustix" +version =3D "1.1.4" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930b= a190" +dependencies =3D [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name =3D "same-file" +version =3D "1.0.6" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b6837890= 0502" +dependencies =3D [ + "winapi-util", +] + +[[package]] +name =3D "scroll" +version =3D "0.13.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e= 7add" +dependencies =3D [ + "scroll_derive", +] + +[[package]] +name =3D "scroll_derive" +version =3D "0.13.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eee= a67d" +dependencies =3D [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name =3D "semver" +version =3D "1.0.28" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae= 51cd" + +[[package]] +name =3D "serde" +version =3D "1.0.228" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124= ae9e" +dependencies =3D [ + "serde_core", + "serde_derive", +] + +[[package]] +name =3D "serde_core" +version =3D "1.0.228" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b= 67ad" +dependencies =3D [ + "serde_derive", +] + +[[package]] +name =3D "serde_derive" +version =3D "1.0.228" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda9= 1d79" +dependencies =3D [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name =3D "serde_json" +version =3D "1.0.149" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e9= 4d86" +dependencies =3D [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name =3D "strsim" +version =3D "0.11.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5= fb4f" + +[[package]] +name =3D "syn" +version =3D "2.0.117" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b= 9b99" +dependencies =3D [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name =3D "tempfile" +version =3D "3.27.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc85= 74bd" +dependencies =3D [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name =3D "unicode-ident" +version =3D "1.0.24" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247= da75" + +[[package]] +name =3D "unicode-xid" +version =3D "0.2.6" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54= f853" + +[[package]] +name =3D "utf8parse" +version =3D "0.2.2" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f101= 0821" + +[[package]] +name =3D "walkdir" +version =3D "2.5.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a= 5e4b" +dependencies =3D [ + "same-file", + "winapi-util", +] + +[[package]] +name =3D "wasip2" +version =3D "1.0.3+wasi-0.2.9" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae2= 23d6" +dependencies =3D [ + "wit-bindgen 0.57.1", +] + +[[package]] +name =3D "wasip3" +version =3D "0.4.0+wasi-0.3.0-rc-2026-01-06" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145= aca5" +dependencies =3D [ + "wit-bindgen 0.51.0", +] + +[[package]] +name =3D "wasm-encoder" +version =3D "0.244.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c= 8319" +dependencies =3D [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name =3D "wasm-metadata" +version =3D "0.244.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296c= c909" +dependencies =3D [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name =3D "wasmparser" +version =3D "0.244.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a6110= 28fe" +dependencies =3D [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name =3D "winapi-util" +version =3D "0.1.11" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba1= 0d22" +dependencies =3D [ + "windows-sys", +] + +[[package]] +name =3D "windows-link" +version =3D "0.2.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a= 4fe5" + +[[package]] +name =3D "windows-sys" +version =3D "0.61.2" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921f= f3fc" +dependencies =3D [ + "windows-link", +] + +[[package]] +name =3D "wit-bindgen" +version =3D "0.51.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fb= caa5" +dependencies =3D [ + "wit-bindgen-rust-macro", +] + +[[package]] +name =3D "wit-bindgen" +version =3D "0.57.1" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614= 536e" + +[[package]] +name =3D "wit-bindgen-core" +version =3D "0.51.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d7= 52dc" +dependencies =3D [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name =3D "wit-bindgen-rust" +version =3D "0.51.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db1= 7f21" +dependencies =3D [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name =3D "wit-bindgen-rust-macro" +version =3D "0.51.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffa= b17a" +dependencies =3D [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name =3D "wit-component" +version =3D "0.244.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb= 9cb2" +dependencies =3D [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name =3D "wit-parser" +version =3D "0.244.0" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656= a736" +dependencies =3D [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name =3D "zmij" +version =3D "1.0.21" +source =3D "registry+https://github.com/rust-lang/crates.io-index" +checksum =3D "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2c= abaa" diff --git a/tools/kapi/Cargo.toml b/tools/kapi/Cargo.toml new file mode 100644 index 0000000000000..3dd36fe412c21 --- /dev/null +++ b/tools/kapi/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name =3D "kapi" +version =3D "0.1.0" +edition =3D "2021" +rust-version =3D "1.78" +authors =3D ["Sasha Levin "] +description =3D "Tool for extracting and displaying kernel API specificati= ons" +license =3D "GPL-2.0" + +[dependencies] +goblin =3D "0.10" +clap =3D { version =3D "4.4", features =3D ["derive"] } +anyhow =3D "1.0" +serde =3D { version =3D "1.0", features =3D ["derive"] } +serde_json =3D "1.0" +regex =3D "1.10" +walkdir =3D "2.4" + +[dev-dependencies] +tempfile =3D "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 ?=3D /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..c0880b8abdc83 --- /dev/null +++ b/tools/kapi/README.md @@ -0,0 +1,32 @@ +# kapi =E2=80=94 Kernel API Specification Extractor + +Userspace utility that extracts and displays kernel API specifications from +three sources: + +- `--source PATH` =E2=80=94 parse kerneldoc blocks in a C source file or t= ree +- `--vmlinux PATH` =E2=80=94 decode the `.kapi_specs` ELF section of a com= piled vmlinux +- `--debugfs PATH` =E2=80=94 read the live specs from `/sys/kernel/debug/k= api/` on a + running kernel (defaults to `/sys/kernel/debug` if no path is given) + +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..a1b8157113eae --- /dev/null +++ b/tools/kapi/src/extractor/debugfs.rs @@ -0,0 +1,849 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +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, ConstraintSpe= c, ErrorSpec, LockSpec, + ParamSpec, ReturnSpec, +}; + +// 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 here and +// interpret them during conversion. +#[derive(Deserialize)] +struct KernelApiJson { + name: String, + #[serde(default)] + api_type: Option, + #[serde(default)] + version: Option, + #[serde(default)] + description: Option, + #[serde(default)] + long_description: Option, + #[serde(default)] + context_flags: Option, + #[serde(default)] + examples: Option, + #[serde(default)] + notes: Option, + #[serde(default)] + capabilities: Option>, + #[serde(default)] + parameters: Option>, + #[serde(default)] + errors: Option>, + #[serde(default, rename =3D "return")] + return_spec: Option, + #[serde(default)] + locks: Option>, + #[serde(default)] + constraints: Option>, + #[serde(default)] + signals: Option>, + #[serde(default)] + side_effects: Option>, +} + +#[derive(Deserialize)] +struct KernelConstraintJson { + name: String, + #[serde(default)] + description: Option, + #[serde(default)] + expression: Option, +} + +#[derive(Deserialize)] +struct KernelSignalJson { + #[serde(default)] + signal_num: i32, + #[serde(default)] + signal_name: Option, + #[serde(default)] + direction: Option, + #[serde(default)] + action: u32, + #[serde(default)] + target: Option, + #[serde(default)] + condition: Option, + #[serde(default)] + description: Option, + #[serde(default)] + restartable: bool, + #[serde(default)] + sa_flags_required: Option, + #[serde(default)] + sa_flags_forbidden: Option, + #[serde(default)] + error_on_signal: i32, + #[serde(default)] + transform_to: i32, + #[serde(default)] + timing: Option, + #[serde(default)] + priority: u32, + #[serde(default)] + interruptible: bool, + #[serde(default)] + queue_behavior: Option, + #[serde(default)] + state_required: Option, + #[serde(default)] + state_forbidden: Option, +} + +#[derive(Deserialize)] +#[allow(dead_code)] +struct KernelSideEffectJson { + #[serde(rename =3D "type", default)] + type_hex: Option, + #[serde(default)] + target: Option, + #[serde(default)] + condition: Option, + #[serde(default)] + description: Option, + #[serde(default)] + reversible: bool, +} + +#[derive(Deserialize)] +#[allow(dead_code)] +struct KernelParamJson { + name: String, + #[serde(rename =3D "type", default)] + type_name: Option, + #[serde(default)] + type_class: Option, + #[serde(default)] + description: Option, + #[serde(default)] + flags: Option, +} + +#[derive(Deserialize)] +struct KernelErrorJson { + #[serde(rename =3D "code")] + error_code: i32, + #[serde(default)] + name: Option, + #[serde(default)] + condition: Option, + #[serde(default)] + description: Option, +} + +#[derive(Deserialize)] +#[allow(dead_code)] +struct KernelReturnJson { + #[serde(rename =3D "type", default)] + type_name: Option, + #[serde(default)] + type_class: Option, + #[serde(default)] + check_type: Option, + #[serde(default)] + description: Option, + #[serde(default)] + success_value: Option, + #[serde(default)] + success_min: Option, + #[serde(default)] + success_max: Option, +} + +#[derive(Deserialize)] +struct KernelLockJson { + name: String, + #[serde(rename =3D "type", default)] + lock_type: Option, + #[serde(default)] + scope: Option, + #[serde(default)] + description: Option, +} + +#[derive(Deserialize)] +struct KernelCapabilityJson { + capability: i32, + name: String, + action: String, + allows: String, + without_cap: String, + check_condition: Option, + priority: Option, + alternatives: Option>, +} + +/// 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) -> Result { + let path =3D match debugfs_path { + Some(p) =3D> PathBuf::from(p), + None =3D> 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 =3D 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> { + let list_path =3D self.debugfs_path.join("kapi/list"); + let content =3D fs::read_to_string(&list_path) + .with_context(|| format!("Failed to read {}", list_path.displa= y()))?; + + let mut apis =3D Vec::new(); + let mut in_list =3D false; + + for line in content.lines() { + if line.contains("=3D=3D=3D") { + in_list =3D 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) =3D 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 { + let mut result =3D Vec::new(); + let bits =3D flags + .strip_prefix("0x") + .or_else(|| flags.strip_prefix("0X")) + .unwrap_or(flags); + let Ok(flags) =3D u32::from_str_radix(bits, 16) else { + return result; + }; + + // These values should match KAPI_CTX_* flags from kernel + if flags & (1 << 0) !=3D 0 { + result.push("PROCESS".to_string()); + } + if flags & (1 << 1) !=3D 0 { + result.push("SOFTIRQ".to_string()); + } + if flags & (1 << 2) !=3D 0 { + result.push("HARDIRQ".to_string()); + } + if flags & (1 << 3) !=3D 0 { + result.push("NMI".to_string()); + } + if flags & (1 << 4) !=3D 0 { + result.push("ATOMIC".to_string()); + } + if flags & (1 << 5) !=3D 0 { + result.push("SLEEPABLE".to_string()); + } + if flags & (1 << 6) !=3D 0 { + result.push("PREEMPT_DISABLED".to_string()); + } + if flags & (1 << 7) !=3D 0 { + result.push("IRQ_DISABLED".to_string()); + } + + result + } + + /// Parse a hex-string like "0x123" into u32, returning 0 on failure. + fn parse_hex_u32(value: &str) -> u32 { + let bits =3D value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + .unwrap_or(value); + u32::from_str_radix(bits, 16).unwrap_or(0) + } + + /// Map the check-type token emitted by the kernel + /// (return_check_type_to_string) back to the u32 enum the formatter w= ants. + fn parse_check_type(token: &str) -> u32 { + match token { + "exact" =3D> 0, + "range" =3D> 1, + "error_check" =3D> 2, + "file_descriptor" =3D> 3, + "custom" =3D> 4, + "no_return" =3D> 5, + _ =3D> 0, + } + } + + /// Map the lock-type token emitted by the kernel (lock_type_to_string= ). + fn parse_lock_type(token: &str) -> u32 { + match token { + "none" =3D> 0, + "mutex" =3D> 1, + "spinlock" =3D> 2, + "rwlock" =3D> 3, + "seqlock" =3D> 4, + "rcu" =3D> 5, + "semaphore" =3D> 6, + "custom" =3D> 7, + _ =3D> 0, + } + } + + /// Map the lock-scope token emitted by the kernel (lock_scope_to_stri= ng). + fn parse_lock_scope(token: &str) -> u32 { + match token { + "internal" =3D> 0, + "acquires" =3D> 1, + "releases" =3D> 2, + "caller_held" =3D> 3, + _ =3D> 0, + } + } + + /// Map the signal-timing token (e.g. "during") back to the u32 enum t= hat + /// 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" =3D> 0, + "during" =3D> 1, + "after" =3D> 2, + _ =3D> 0, + } + } + + /// Convert capability action from kernel representation + fn parse_capability_action(action: &str) -> String { + match action { + "bypass_check" =3D> "Bypasses check".to_string(), + "increase_limit" =3D> "Increases limit".to_string(), + "override_restriction" =3D> "Overrides restriction".to_string(= ), + "grant_permission" =3D> "Grants permission".to_string(), + "modify_behavior" =3D> "Modifies behavior".to_string(), + "access_resource" =3D> "Allows resource access".to_string(), + "perform_operation" =3D> "Allows operation".to_string(), + _ =3D> action.to_string(), + } + } + + /// Try to parse as JSON first + fn try_parse_json(&self, content: &str) -> Option { + let json_data: KernelApiJson =3D serde_json::from_str(content).ok(= )?; + + let mut spec =3D ApiSpec { + name: json_data.name, + api_type: json_data.api_type.unwrap_or_else(|| "syscall".to_st= ring()), + description: json_data.description, + long_description: 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: json_data.examples, + notes: json_data.notes, + subsystem: None, // Not in current JSON format + sysfs_path: None, // Not in current JSON format + permissions: None, // Not in current 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) =3D 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: cap.check_condition, + priority: cap.priority, + alternatives: cap.alternatives.unwrap_or_default(), + }); + } + } + + // Convert parameters. kapi_json_str() passes through trailing + // whitespace that the macro generator may leave on description + // strings; trim here to match --source / --vmlinux. + if let Some(params) =3D json_data.parameters { + for (i, p) in params.into_iter().enumerate() { + let flags =3D p.flags.as_deref().map_or(0, Self::parse_hex= _u32); + spec.parameters.push(ParamSpec { + index: i as u32, + name: p.name, + type_name: p.type_name.unwrap_or_default(), + description: p + .description + .map(|s| s.trim_end().to_string()) + .unwrap_or_default(), + flags, + 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, + }); + } + spec.param_count =3D Some(spec.parameters.len() as u32); + } + + // Convert errors + if let Some(errors) =3D 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(), + }); + } + spec.error_count =3D Some(spec.errors.len() as u32); + } + + // Convert return spec + if let Some(ret) =3D json_data.return_spec { + let check_type =3D ret.check_type.as_deref().map_or(0, Self::p= arse_check_type); + spec.return_spec =3D Some(ReturnSpec { + type_name: ret.type_name.unwrap_or_default(), + description: ret.description.unwrap_or_default(), + return_type: 0, + check_type, + success_value: ret.success_value, + success_min: ret.success_min, + success_max: ret.success_max, + error_values: vec![], + }); + } + + // Convert locks + if let Some(locks) =3D json_data.locks { + for l in locks { + let lock_type =3D l.lock_type.as_deref().map_or(0, Self::p= arse_lock_type); + let scope =3D l.scope.as_deref().map_or(0, Self::parse_loc= k_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) =3D 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. The kernel-side kapi_json_str() emits NULL + // char * as the empty string "", so normalise empty -> None here + // to match the ApiSpec convention used by --source / --vmlinux. + fn opt_str(s: Option) -> Option { + s.filter(|v| !v.is_empty()) + } + if let Some(signals) =3D json_data.signals { + for s in signals { + let direction =3D s.direction.as_deref().map_or(0, Self::p= arse_hex_u32); + let sa_flags_required =3D s + .sa_flags_required + .as_deref() + .map_or(0, Self::parse_hex_u32); + let sa_flags_forbidden =3D s + .sa_flags_forbidden + .as_deref() + .map_or(0, Self::parse_hex_u32); + let state_required =3D s.state_required.as_deref().map_or(= 0, Self::parse_hex_u32); + let state_forbidden =3D s.state_forbidden.as_deref().map_o= r(0, Self::parse_hex_u32); + let timing =3D s.timing.as_deref().map_or(0, Self::parse_s= ignal_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 !=3D 0 { + Some(s.error_on_signal) + } else { + None + }, + transform_to: if s.transform_to !=3D 0 { + // Kernel JSON already carries the numeric value. + Some(s.transform_to) + } else { + None + }, + }); + } + } + + // Convert side effects. + if let Some(effects) =3D json_data.side_effects { + for e in effects { + let effect_type =3D 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, + }); + } + } + + Some(spec) + } + + /// Parse a single API specification file + fn parse_spec_file(&self, api_name: &str) -> Result { + // Prefer the JSON endpoint if the kernel exposes it (added togeth= er + // with the framework). Fall back to parsing the plain-text dump f= or + // older kernels that only provide /sys/kernel/debug/kapi/specs/. + let json_path =3D self + .debugfs_path + .join(format!("kapi/specs-json/{}", api_name)); + if let Ok(content) =3D fs::read_to_string(&json_path) { + if let Some(spec) =3D self.try_parse_json(&content) { + return Ok(spec); + } + } + + let spec_path =3D self.debugfs_path.join(format!("kapi/specs/{}", = api_name)); + let content =3D fs::read_to_string(&spec_path) + .with_context(|| format!("Failed to read {}", spec_path.displa= y()))?; + + // Older kernels may still emit JSON via specs/ if someone backpor= ted it. + if let Some(spec) =3D self.try_parse_json(&content) { + return Ok(spec); + } + + // Fall back to plain text parsing + let mut spec =3D 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 collecting_multiline =3D false; + let mut multiline_buffer =3D String::new(); + let mut multiline_field =3D ""; + let mut parsing_capability =3D false; + let mut in_capabilities_section =3D false; + let mut current_capability: Option =3D None; + + for line in content.lines() { + // Handle capability sections + if line.starts_with("Capabilities (") { + in_capabilities_section =3D true; + continue; + } + // Any other top-level section header ends the capabilities se= ction + // 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_wit= h(':') { + in_capabilities_section =3D 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) =3D current_capability.take() { + spec.capabilities.push(cap); + } + + let parts: Vec<&str> =3D line.trim().split(" (").collect(); + if parts.len() =3D=3D 2 { + let cap_name =3D parts[0].to_string(); + let cap_id =3D parts[1].trim_end_matches("):").parse()= .unwrap_or(0); + current_capability =3D 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 =3D true; + } + continue; + } + if parsing_capability && line.starts_with(" ") { + // Parse capability fields + if let Some(ref mut cap) =3D current_capability { + if let Some(action) =3D line.strip_prefix(" Action:= ") { + cap.action =3D action.to_string(); + } else if let Some(allows) =3D line.strip_prefix(" = Allows: ") { + cap.allows =3D allows.to_string(); + } else if let Some(without) =3D line.strip_prefix(" = Without: ") { + cap.without_cap =3D without.to_string(); + } else if let Some(cond) =3D line.strip_prefix(" Co= ndition: ") { + cap.check_condition =3D Some(cond.to_string()); + } else if let Some(prio) =3D line.strip_prefix(" Pr= iority: ") { + cap.priority =3D prio.parse().ok(); + } else if let Some(alts) =3D line.strip_prefix(" Al= ternatives: ") { + cap.alternatives =3D + alts.split(", ").filter_map(|s| s.parse().ok()= ).collect(); + } + } + continue; + } + if parsing_capability && !line.starts_with(" ") { + // End of capabilities section + if let Some(cap) =3D current_capability.take() { + spec.capabilities.push(cap); + } + parsing_capability =3D false; + } + + // Handle section headers + if line.starts_with("Parameters (") { + if let Some(count_str) =3D line + .strip_prefix("Parameters (") + .and_then(|s| s.strip_suffix("):")) + { + spec.param_count =3D count_str.parse().ok(); + } + continue; + } else if line.starts_with("Errors (") { + if let Some(count_str) =3D line + .strip_prefix("Errors (") + .and_then(|s| s.strip_suffix("):")) + { + spec.error_count =3D count_str.parse().ok(); + } + continue; + } else if line.starts_with("Examples:") { + collecting_multiline =3D true; + multiline_field =3D "examples"; + multiline_buffer.clear(); + continue; + } else if line.starts_with("Notes:") { + collecting_multiline =3D true; + multiline_field =3D "notes"; + multiline_buffer.clear(); + continue; + } + + // Handle multiline sections + if collecting_multiline { + // Terminate multiline on known field patterns or double b= lank line + let is_field =3D line.starts_with("Description: ") + || line.starts_with("Long description: ") + || line.starts_with("Version: ") + || line.starts_with("Context flags: ") + || line.starts_with("Subsystem: ") + || line.starts_with("Sysfs Path: ") + || line.starts_with("Permissions: ") + || line.starts_with("Parameters (") + || line.starts_with("Errors (") + || line.starts_with("Capabilities ("); + if is_field || (line.trim().is_empty() && multiline_buffer= .ends_with("\n\n")) { + collecting_multiline =3D false; + match multiline_field { + "examples" =3D> spec.examples =3D Some(multiline_b= uffer.trim().to_string()), + "notes" =3D> spec.notes =3D Some(multiline_buffer.= trim().to_string()), + _ =3D> {} + } + multiline_buffer.clear(); + if !is_field { + continue; + } + // Fall through to parse this line as a field + } else { + if !multiline_buffer.is_empty() { + multiline_buffer.push('\n'); + } + multiline_buffer.push_str(line); + continue; + } + } + + // Parse regular fields + if let Some(desc) =3D line.strip_prefix("Description: ") { + spec.description =3D Some(desc.to_string()); + } else if let Some(long_desc) =3D line.strip_prefix("Long desc= ription: ") { + spec.long_description =3D Some(long_desc.to_string()); + } else if let Some(version) =3D line.strip_prefix("Version: ")= { + spec.version =3D Some(version.to_string()); + } else if let Some(flags) =3D line.strip_prefix("Context flags= : ") { + spec.context_flags =3D flags.split_whitespace().map(str::t= o_string).collect(); + } else if let Some(subsys) =3D line.strip_prefix("Subsystem: "= ) { + spec.subsystem =3D Some(subsys.to_string()); + } else if let Some(path) =3D line.strip_prefix("Sysfs Path: ")= { + spec.sysfs_path =3D Some(path.to_string()); + } else if let Some(perms) =3D line.strip_prefix("Permissions: = ") { + spec.permissions =3D Some(perms.to_string()); + } + } + + // Flush any remaining multiline buffer + if collecting_multiline { + match multiline_field { + "examples" =3D> spec.examples =3D Some(multiline_buffer.tr= im().to_string()), + "notes" =3D> spec.notes =3D Some(multiline_buffer.trim().t= o_string()), + _ =3D> {} + } + } + + // Handle any remaining capability + if let Some(cap) =3D current_capability.take() { + spec.capabilities.push(cap); + } + + // Determine API type based on name + if api_name.starts_with("sys_") { + spec.api_type =3D "syscall".to_string(); + } else if api_name.contains("_ioctl") || api_name.starts_with("ioc= tl_") { + spec.api_type =3D "ioctl".to_string(); + } else if api_name.contains("sysfs") + || api_name.ends_with("_show") + || api_name.ends_with("_store") + { + spec.api_type =3D "sysfs".to_string(); + } else { + spec.api_type =3D "function".to_string(); + } + + Ok(spec) + } +} + +impl ApiExtractor for DebugfsExtractor { + fn extract_all(&self) -> Result> { + let api_names =3D self.parse_list_file()?; + let mut specs =3D Vec::new(); + + for name in api_names { + match self.parse_spec_file(&name) { + Ok(spec) =3D> specs.push(spec), + Err(e) =3D> { + eprintln!("Warning: failed to parse API spec '{}': {}"= , name, e); + } + } + } + + Ok(specs) + } + + fn extract_by_name(&self, name: &str) -> Result> { + let api_names =3D 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) =3D self.extract_by_name(api_name)? { + display_api_spec(&spec, formatter, writer)?; + } else { + writeln!(writer, "API '{api_name}' not found in debugfs")?; + } + + Ok(()) + } +} diff --git a/tools/kapi/src/extractor/kerneldoc_parser.rs b/tools/kapi/src/= extractor/kerneldoc_parser.rs new file mode 100644 index 0000000000000..f67110007d86f --- /dev/null +++ b/tools/kapi/src/extractor/kerneldoc_parser.rs @@ -0,0 +1,2831 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +use super::{ + ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpe= c, ReturnSpec, + SideEffectSpec, SignalSpec, StateTransitionSpec, StructFieldSpec, Stru= ctSpec, +}; +use anyhow::Result; +use std::collections::HashMap; + +/// Real kerneldoc parser that extracts KAPI annotations +pub struct KerneldocParserImpl; + +/// What block are we currently inside? +#[derive(Debug, Clone, PartialEq)] +enum BlockContext { + None, + Param(String), // param: + Error(String), // error: + Signal, // signal: + Capability, // capability: + SideEffect, // side-effect: + StateTransition, // state-trans: ... + Constraint, // constraint: + Lock, // lock: + Return, // return: +} + +/// Parse a numeric literal, supporting plain decimal and 0x-prefixed hex. +/// 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 "mask unknown" and leave the downstream slot unset, not +/// store it as 0 =E2=80=94 which would wrongly assert that zero bits are = valid. +fn parse_u64_literal(s: &str) -> Option { + let t =3D s.trim(); + if let Some(hex) =3D t.strip_prefix("0x").or_else(|| t.strip_prefix("0= X")) { + u64::from_str_radix(hex, 16).ok() + } else { + t.parse().ok() + } +} + +/// `true` if `s` contains more '(' than ')' when scanned left-to-right. +/// Used to decide whether the caller needs to pull more continuation +/// lines before trying to parse a constraint expression. +fn has_unbalanced_paren(s: &str) -> bool { + let mut depth: i32 =3D 0; + for c in s.chars() { + match c { + '(' =3D> depth +=3D 1, + ')' =3D> depth -=3D 1, + _ =3D> {} + } + } + depth > 0 +} + +/// Canonicalise a kerneldoc `type:` value to its KAPI_TYPE_* spelling. +/// Used in the `return:` block so `type_name` carries the long form +/// regardless of which spelling the source used. +fn canon_kapi_type_name(s: &str) -> String { + let t =3D s.trim(); + if t.starts_with("KAPI_TYPE_") { + return t.to_string(); + } + match t.to_ascii_lowercase().as_str() { + "void" =3D> "KAPI_TYPE_VOID".to_string(), + "int" =3D> "KAPI_TYPE_INT".to_string(), + "uint" =3D> "KAPI_TYPE_UINT".to_string(), + "ptr" =3D> "KAPI_TYPE_PTR".to_string(), + "struct" =3D> "KAPI_TYPE_STRUCT".to_string(), + "union" =3D> "KAPI_TYPE_UNION".to_string(), + "enum" =3D> "KAPI_TYPE_ENUM".to_string(), + "func_ptr" =3D> "KAPI_TYPE_FUNC_PTR".to_string(), + "array" =3D> "KAPI_TYPE_ARRAY".to_string(), + "fd" =3D> "KAPI_TYPE_FD".to_string(), + "user_ptr" | "uptr" =3D> "KAPI_TYPE_USER_PTR".to_string(), + "path" =3D> "KAPI_TYPE_PATH".to_string(), + "custom" =3D> "KAPI_TYPE_CUSTOM".to_string(), + _ =3D> t.to_string(), + } +} + +/// Canonicalise a capability `type:` value to its KAPI_CAP_* spelling. +fn canon_kapi_cap_action(s: &str) -> String { + let t =3D s.trim(); + if t.starts_with("KAPI_CAP_") { + return t.to_string(); + } + match t.to_ascii_lowercase().as_str() { + "bypass_check" =3D> "KAPI_CAP_BYPASS_CHECK".to_string(), + "increase_limit" =3D> "KAPI_CAP_INCREASE_LIMIT".to_string(), + "override_restriction" =3D> "KAPI_CAP_OVERRIDE_RESTRICTION".to_str= ing(), + "grant_permission" =3D> "KAPI_CAP_GRANT_PERMISSION".to_string(), + "modify_behavior" =3D> "KAPI_CAP_MODIFY_BEHAVIOR".to_string(), + "access_resource" =3D> "KAPI_CAP_ACCESS_RESOURCE".to_string(), + "perform_operation" =3D> "KAPI_CAP_PERFORM_OPERATION".to_string(), + _ =3D> 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" + ) +} + +/// Return true if the line's first whitespace-delimited token is a +/// bare identifier ending in ':' (e.g. `type:`, `constraint-type:`, +/// `error:`). Used by the continuation folder to stop at the next +/// block attribute. +fn is_block_key(s: &str) -> bool { + let head =3D s.split_whitespace().next().unwrap_or(""); + if !head.ends_with(':') || head.len() < 2 { + return false; + } + let ident =3D &head[..head.len() - 1]; + !ident.is_empty() + && ident + .chars() + .all(|c| c.is_ascii_alphanumeric() || c =3D=3D '_' || c =3D=3D= '-') +} + +impl KerneldocParserImpl { + pub fn new() -> Self { + KerneldocParserImpl + } + + pub fn parse_kerneldoc( + &self, + doc: &str, + name: &str, + api_type: &str, + signature: Option<&str>, + ) -> Result { + let mut spec =3D ApiSpec { + name: name.to_string(), + api_type: api_type.to_string(), + ..Default::default() + }; + + let lines: Vec<&str> =3D doc.lines().collect(); + + // Extract main description from function name line + if let Some(first_line) =3D lines.first() { + if let Some((_, desc)) =3D first_line.split_once(" - ") { + spec.description =3D Some(desc.trim().to_string()); + } + } + + // Extract type names from SYSCALL_DEFINE signature + let type_map =3D if let Some(sig) =3D 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 =3D HashMap::new(); + let mut struct_fields: Vec =3D Vec::new(); + + // Current block being parsed + let mut block =3D BlockContext::None; + + // Temporary storage for current block items + let mut current_lock: Option =3D None; + let mut current_signal: Option =3D 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 =3D None; + let mut current_capability: Option =3D None; + let mut current_side_effect: Option =3D None; + let mut current_constraint: Option =3D None; + let mut current_error: Option =3D None; + let mut current_return: Option =3D None; + + let mut i =3D 0; + + while i < lines.len() { + let line =3D lines[i]; + let trimmed =3D line.trim(); + + // Skip empty lines + if trimmed.is_empty() { + i +=3D 1; + continue; + } + + // Check if this is an indented continuation line (part of cur= rent block) + let is_indented =3D line.starts_with(" ") || line.starts_with= ('\t'); + + // If indented and we're in a block, parse as block attribute. + // Before dispatching, fold continuation lines into `trimmed` + // when the value has an unbalanced '(' so that expressions + // like `constraint-type: mask(FOO | BAR |` ... `| BAZ)` + // arrive as a single logical line. + if is_indented && block !=3D BlockContext::None { + let mut folded: Option =3D None; + if has_unbalanced_paren(trimmed) { + let mut buf =3D trimmed.to_string(); + let mut j =3D i + 1; + while j < lines.len() { + let next =3D lines[j]; + let next_trim =3D next.trim(); + if next_trim.is_empty() { + break; + } + if !(next.starts_with(" ") || next.starts_with('\= t')) { + break; + } + // Stop if we've hit another known key + if is_block_key(next_trim) { + break; + } + buf.push(' '); + buf.push_str(next_trim); + j +=3D 1; + if !has_unbalanced_paren(&buf) { + break; + } + } + if j > i + 1 { + i =3D j - 1; // outer loop will +=3D 1 + folded =3D Some(buf); + } + } + let line_to_parse: &str =3D folded.as_deref().unwrap_or(tr= immed); + self.parse_block_attribute( + line_to_parse, + &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, + ); + i +=3D 1; + continue; + } + + // Not indented or not in block =E2=80=94 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) =3D pending_transform_warning.take() { + eprintln!( + "kapi: warning: transform-to: {raw:?} is symbolic;= \ + source-mode cannot resolve signal numbers portabl= y. \ + Use --vmlinux or --debugfs to get the resolved va= lue.", + ); + } + } + 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, + ); + + // Parse top-level annotations + if let Some(rest) =3D trimmed.strip_prefix("@") { + // @param: description =E2=80=94 standard kerneldoc parame= ter + if let Some((param_name, desc)) =3D rest.split_once(':') { + let param_name =3D param_name.trim(); + let desc =3D desc.trim(); + if !param_name.contains('-') { + let idx =3D param_map.len() as u32; + let type_name =3D 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) =3D trimmed.strip_prefix("long-desc:"= ) { + let (val, next_i) =3D self.collect_multiline_value(&lines,= i, rest); + spec.long_description =3D Some(val); + i =3D next_i; + continue; + } else if let Some(rest) =3D trimmed.strip_prefix("context-fla= gs:") { + spec.context_flags =3D self.parse_context_flags(rest.trim(= )); + } else if let Some(rest) =3D trimmed.strip_prefix("contexts:")= { + // Short form: "contexts: process, sleepable" + spec.context_flags =3D self.parse_context_list(rest.trim()= ); + } else if let Some(rest) =3D trimmed.strip_prefix("param-count= :") { + spec.param_count =3D rest.trim().parse().ok(); + } + // Flat param-* annotations (alternative format) + else if let Some(rest) =3D trimmed.strip_prefix("param-type:")= { + let parts: Vec<&str> =3D rest.split(',').map(|s| s.trim())= .collect(); + if parts.len() >=3D 2 { + if let Some(param) =3D param_map.get_mut(parts[0]) { + param.param_type =3D self.parse_param_type(parts[1= ]); + } + } + } else if let Some(rest) =3D trimmed.strip_prefix("param-flags= :") { + let parts: Vec<&str> =3D rest.split(',').map(|s| s.trim())= .collect(); + if parts.len() >=3D 2 { + if let Some(param) =3D param_map.get_mut(parts[0]) { + param.flags =3D self.parse_param_flags(parts[1]); + } + } + } else if let Some(rest) =3D trimmed.strip_prefix("param-range= :") { + let parts: Vec<&str> =3D rest.split(',').map(|s| s.trim())= .collect(); + if parts.len() >=3D 3 { + if let Some(param) =3D param_map.get_mut(parts[0]) { + param.min_value =3D parts[1].parse().ok(); + param.max_value =3D parts[2].parse().ok(); + param.constraint_type =3D 1; // KAPI_CONSTRAINT_RA= NGE + } + } + } else if let Some(rest) =3D trimmed.strip_prefix("param-const= raint:") { + let parts: Vec<&str> =3D rest.splitn(2, ',').map(|s| s.tri= m()).collect(); + if parts.len() >=3D 2 { + if let Some(param) =3D param_map.get_mut(parts[0]) { + param.constraint =3D Some(parts[1].to_string()); + } + } + } + // Block-start annotations + else if let Some(rest) =3D trimmed.strip_prefix("param:") { + let param_name =3D rest.trim().to_string(); + block =3D BlockContext::Param(param_name.clone()); + // Ensure param exists in map + if !param_map.contains_key(¶m_name) { + let idx =3D param_map.len() as u32; + let type_name =3D 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) =3D trimmed.strip_prefix("error:") { + // error: NAME, condition + let parts: Vec<&str> =3D rest.splitn(2, ',').map(|s| s.tri= m()).collect(); + if !parts.is_empty() { + let error_name =3D parts[0].to_string(); + let condition =3D if parts.len() >=3D 2 { + parts[1].to_string() + } else { + String::new() + }; + let error_code =3D self.error_name_to_code(&error_name= ); + current_error =3D Some(ErrorSpec { + error_code, + name: error_name.clone(), + condition, + description: String::new(), + }); + block =3D BlockContext::Error(error_name); + } + } else if let Some(rest) =3D trimmed.strip_prefix("signal:") { + let signal_name =3D rest.trim().to_string(); + current_signal =3D 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 =3D BlockContext::Signal; + } else if let Some(rest) =3D trimmed.strip_prefix("capability:= ") { + let parts: Vec<&str> =3D rest.split(',').map(|s| s.trim())= .collect(); + if !parts.is_empty() { + let cap_name =3D parts[0].to_string(); + let cap_value =3D self.parse_capability_value(&cap_nam= e); + // If we have 3 parts, it's flat format: capability: C= AP, action, name + let (action, name) =3D if parts.len() >=3D 3 { + (parts[1].to_string(), parts[2].to_string()) + } else { + (String::new(), cap_name.clone()) + }; + current_capability =3D Some(CapabilitySpec { + capability: cap_value, + name, + action, + allows: String::new(), + without_cap: String::new(), + check_condition: None, + priority: Some(0), + alternatives: vec![], + }); + block =3D BlockContext::Capability; + } + } else if let Some(rest) =3D trimmed.strip_prefix("side-effect= :") { + // Could be flat format (comma-separated) or block start + let rest =3D rest.trim(); + // Check if it's the flat format with commas + let comma_parts: Vec<&str> =3D rest.splitn(3, ',').map(|s|= s.trim()).collect(); + if comma_parts.len() >=3D 3 { + // Flat format: side-effect: TYPE, target, desc + let mut effect =3D 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=3Dyes") { + effect.reversible =3D true; + } + spec.side_effects.push(effect); + } else { + // Block format: side-effect: TYPE + current_side_effect =3D Some(SideEffectSpec { + effect_type: self.parse_effect_type(rest), + target: String::new(), + condition: None, + description: String::new(), + reversible: false, + }); + block =3D BlockContext::SideEffect; + } + } else if let Some(rest) =3D trimmed.strip_prefix("state-trans= :") { + let parts: Vec<&str> =3D rest.split(',').map(|s| s.trim())= .collect(); + if parts.len() >=3D 4 { + spec.state_transitions.push(StateTransitionSpec { + object: parts[0].to_string(), + from_state: parts[1].to_string(), + to_state: parts[2].to_string(), + condition: None, + description: parts[3].to_string(), + }); + } + block =3D BlockContext::StateTransition; + } else if let Some(rest) =3D trimmed.strip_prefix("constraint:= ") { + let rest =3D rest.trim(); + // Could be flat format: constraint: name, desc + // Or block format: constraint: name + let parts: Vec<&str> =3D rest.splitn(2, ',').map(|s| s.tri= m()).collect(); + if parts.len() >=3D 2 { + // Flat format + current_constraint =3D Some(ConstraintSpec { + name: parts[0].to_string(), + description: parts[1].to_string(), + expression: None, + }); + } else { + // Block format + current_constraint =3D Some(ConstraintSpec { + name: rest.to_string(), + description: String::new(), + expression: None, + }); + } + block =3D BlockContext::Constraint; + } else if let Some(rest) =3D trimmed.strip_prefix("constraint-= expr:") { + // Flat format: constraint-expr: name, expr + let parts: Vec<&str> =3D rest.splitn(2, ',').map(|s| s.tri= m()).collect(); + if parts.len() >=3D 2 { + if let Some(constraint) =3D + spec.constraints.iter_mut().find(|c| c.name =3D=3D= parts[0]) + { + constraint.expression =3D Some(parts[1].to_string(= )); + } + } + } else if let Some(rest) =3D trimmed.strip_prefix("lock:") { + let rest =3D rest.trim(); + // Could be flat: lock: name, type + // Or block: lock: name + let parts: Vec<&str> =3D rest.split(',').map(|s| s.trim())= .collect(); + if parts.len() >=3D 2 { + current_lock =3D 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 =3D Some(LockSpec { + lock_name: rest.to_string(), + lock_type: 0, + scope: super::KAPI_LOCK_INTERNAL, + description: String::new(), + }); + } + block =3D BlockContext::Lock; + } + // Flat signal-* attributes (alternative format) + else if let Some(rest) =3D trimmed.strip_prefix("signal-direct= ion:") { + if let Some(signal) =3D current_signal.as_mut() { + signal.direction =3D self.parse_signal_direction(rest.= trim()); + } + } else if let Some(rest) =3D trimmed.strip_prefix("signal-acti= on:") { + if let Some(signal) =3D current_signal.as_mut() { + signal.action =3D self.parse_signal_action(rest.trim()= ); + } + } else if let Some(rest) =3D trimmed.strip_prefix("signal-cond= ition:") { + if let Some(signal) =3D current_signal.as_mut() { + let (val, next_i) =3D self.collect_multiline_value(&li= nes, i, rest); + signal.condition =3D Some(val); + i =3D next_i; + continue; + } + } else if let Some(rest) =3D trimmed.strip_prefix("signal-desc= :") { + if let Some(signal) =3D current_signal.as_mut() { + let (val, next_i) =3D self.collect_multiline_value(&li= nes, i, rest); + signal.description =3D Some(val); + i =3D next_i; + continue; + } + } else if let Some(rest) =3D trimmed.strip_prefix("signal-timi= ng:") { + if let Some(signal) =3D current_signal.as_mut() { + signal.timing =3D self.parse_signal_timing(rest.trim()= ); + } + } else if let Some(rest) =3D trimmed.strip_prefix("signal-prio= rity:") { + if let Some(signal) =3D current_signal.as_mut() { + signal.priority =3D rest.trim().parse().unwrap_or(0); + } + } else if let Some(rest) =3D trimmed.strip_prefix("signal-inte= rruptible:") { + if let Some(signal) =3D current_signal.as_mut() { + let val =3D rest.trim().to_lowercase(); + signal.interruptible =3D !matches!(val.as_str(), "no" = | "false" | "0"); + } + } else if let Some(rest) =3D trimmed.strip_prefix("signal-stat= e-req:") { + if let Some(signal) =3D current_signal.as_mut() { + signal.state_required =3D self.parse_signal_state(rest= .trim()); + } + } + // Flat capability-* attributes + else if let Some(rest) =3D trimmed.strip_prefix("capability-al= lows:") { + if let Some(cap) =3D current_capability.as_mut() { + let (val, next_i) =3D self.collect_multiline_value(&li= nes, i, rest); + cap.allows =3D val; + i =3D next_i; + continue; + } + } else if let Some(rest) =3D trimmed.strip_prefix("capability-= without:") { + if let Some(cap) =3D current_capability.as_mut() { + let (val, next_i) =3D self.collect_multiline_value(&li= nes, i, rest); + cap.without_cap =3D val; + i =3D next_i; + continue; + } + } else if let Some(rest) =3D trimmed.strip_prefix("capability-= condition:") { + if let Some(cap) =3D current_capability.as_mut() { + let (val, next_i) =3D self.collect_multiline_value(&li= nes, i, rest); + cap.check_condition =3D Some(val); + i =3D next_i; + continue; + } + } else if let Some(rest) =3D trimmed.strip_prefix("capability-= priority:") { + if let Some(cap) =3D current_capability.as_mut() { + cap.priority =3D rest.trim().parse().ok(); + } + } + // Lock flat attributes + else if let Some(rest) =3D trimmed.strip_prefix("lock-scope:")= { + if let Some(lock) =3D current_lock.as_mut() { + lock.scope =3D match rest.trim() { + "internal" =3D> super::KAPI_LOCK_INTERNAL, + "acquires" =3D> super::KAPI_LOCK_ACQUIRES, + "releases" =3D> super::KAPI_LOCK_RELEASES, + "caller_held" =3D> super::KAPI_LOCK_CALLER_HELD, + _ =3D> super::KAPI_LOCK_INTERNAL, + }; + } + } else if let Some(rest) =3D trimmed.strip_prefix("lock-desc:"= ) { + if let Some(lock) =3D current_lock.as_mut() { + let (val, next_i) =3D self.collect_multiline_value(&li= nes, i, rest); + lock.description =3D val; + i =3D next_i; + continue; + } + } + // Struct field annotations + else if let Some(rest) =3D trimmed.strip_prefix("struct-field:= ") { + let parts: Vec<&str> =3D rest.split(',').map(|s| s.trim())= .collect(); + if parts.len() >=3D 3 { + struct_fields.push(StructFieldSpec { + name: parts[0].to_string(), + field_type: self.parse_field_type(parts[1]), + type_name: parts[1].to_string(), + offset: 0, + size: 0, + flags: 0, + constraint_type: 0, + min_value: 0, + max_value: 0, + valid_mask: 0, + description: parts[2].to_string(), + }); + } + } else if let Some(rest) =3D trimmed.strip_prefix("struct-fiel= d-range:") { + let parts: Vec<&str> =3D rest.split(',').map(|s| s.trim())= .collect(); + if parts.len() >=3D 3 { + if let Some(field) =3D struct_fields.iter_mut().find(|= f| f.name =3D=3D parts[0]) { + field.min_value =3D parts[1].parse().unwrap_or(0); + field.max_value =3D parts[2].parse().unwrap_or(0); + field.constraint_type =3D 1; + } + } + } + // Other top-level annotations + else if let Some(rest) =3D trimmed.strip_prefix("return:") { + let rest =3D rest.trim(); + if rest.is_empty() { + // Block format + current_return =3D 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 =3D BlockContext::Return; + } + } else if let Some(rest) =3D trimmed.strip_prefix("return-type= :") { + if spec.return_spec.is_none() { + spec.return_spec =3D Some(ReturnSpec { + type_name: rest.trim().to_string(), + description: String::new(), + return_type: self.parse_param_type(rest.trim()), + check_type: 0, + success_value: None, + success_min: None, + success_max: None, + error_values: vec![], + }); + } + } else if let Some(rest) =3D trimmed.strip_prefix("return-chec= k-type:") { + if let Some(ret) =3D spec.return_spec.as_mut() { + ret.check_type =3D self.parse_return_check_type(rest.t= rim()); + } + } else if let Some(rest) =3D trimmed.strip_prefix("return-succ= ess:") { + if let Some(ret) =3D spec.return_spec.as_mut() { + ret.success_value =3D rest.trim().parse().ok(); + } + } else if let Some(rest) =3D trimmed.strip_prefix("examples:")= { + let (val, next_i) =3D self.collect_multiline_value(&lines,= i, rest); + spec.examples =3D Some(val); + i =3D next_i; + continue; + } else if let Some(rest) =3D trimmed.strip_prefix("notes:") { + let (val, next_i) =3D self.collect_multiline_value(&lines,= i, rest); + spec.notes =3D Some(val); + i =3D next_i; + continue; + } + + i +=3D 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) =3D 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, + ); + + // Convert param_map to vec preserving order + let mut params: Vec =3D 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. + // param-count: is otherwise redundant with the block count, and + // new short-form specs should just drop it. + if let Some(claimed) =3D spec.param_count { + if claimed as usize !=3D params.len() { + eprintln!( + "kapi: {}: param-count: {} disagrees with {} param: bl= ock(s)", + name, + claimed, + params.len(), + ); + } + } + + spec.parameters =3D params; + + // Create struct spec if we have fields + if !struct_fields.is_empty() { + spec.struct_specs.push(StructSpec { + name: format!("struct {name}"), + size: 0, + alignment: 0, + field_count: struct_fields.len() as u32, + fields: struct_fields, + description: "Structure specification".to_string(), + }); + } + + 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, + current_error: &mut Option, + current_signal: &mut Option, + pending_transform_warning: &mut Option, + current_capability: &mut Option, + current_side_effect: &mut Option, + current_constraint: &mut Option, + current_lock: &mut Option, + current_return: &mut Option, + ) { + match block { + BlockContext::Param(param_name) =3D> { + if let Some(param) =3D param_map.get_mut(param_name) { + if let Some(rest) =3D trimmed.strip_prefix("type:") { + // Accept either: + // type: KAPI_TYPE_UINT (long, sing= le token) + // type: uint (short, sin= gle token) + // type: uint, input (short, typ= e + flags) + // type: path, input (short, typ= e + flags) + // Single-token inputs leave flags alone so existi= ng + // long-form specs that use a separate `flags:` li= ne + // keep working unchanged. + // + // 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 =3D rest.split(',').map(str::trim); + let type_token =3D parts.next(); + if let Some(ty) =3D type_token { + param.param_type =3D self.parse_param_type(ty); + } + for flag in parts { + param.flags |=3D self.parse_param_flag_token(f= lag); + } + if type_token.map(type_implies_user_flag).unwrap_o= r(false) { + param.flags |=3D 1 << 6; // KAPI_PARAM_USER + } + } else if let Some(rest) =3D trimmed.strip_prefix("fla= gs:") { + param.flags =3D self.parse_param_flags(rest.trim()= ); + } else if let Some(rest) =3D trimmed.strip_prefix("con= straint-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 =3D rest.trim(); + if !self.apply_constraint_expr(param, text) { + param.constraint_type =3D self.parse_constrain= t_type(text); + } + } else if let Some(rest) =3D trimmed.strip_prefix("val= id-mask:") { + // Symbolic mask values need cpp-level resolution; + // leave that to the binary reader. + let _ =3D rest; + } else if let Some(rest) =3D trimmed.strip_prefix("con= straint:") { + // Free-text constraint description; multiline app= end. + let text =3D rest.trim(); + if param.constraint.is_none() { + param.constraint =3D Some(text.to_string()); + } else if let Some(c) =3D param.constraint.as_mut(= ) { + c.push(' '); + c.push_str(text); + } + } else if let Some(rest) =3D trimmed.strip_prefix("ran= ge:") { + let parts: Vec<&str> =3D rest.split(',').map(|s| s= .trim()).collect(); + if parts.len() >=3D 2 { + param.min_value =3D parts[0].parse().ok(); + param.max_value =3D parts[1].parse().ok(); + param.constraint_type =3D 1; // KAPI_CONSTRAIN= T_RANGE + } + } else if let Some(rest) =3D trimmed.strip_prefix("siz= e-param:") { + param.size_param_idx =3D rest.trim().parse().ok(); + } else if let Some(rest) =3D trimmed.strip_prefix("des= cription:") { + param.description =3D rest.trim().to_string(); + } else if let Some(rest) =3D trimmed.strip_prefix("des= c:") { + param.description =3D rest.trim().to_string(); + } else if !trimmed.contains(':') || trimmed.starts_wit= h(" ") { + // Continuation of the previous attribute's value. + if let Some(c) =3D param.constraint.as_mut() { + c.push(' '); + c.push_str(trimmed); + } + } + } + } + BlockContext::Error(_) =3D> { + if let Some(error) =3D current_error.as_mut() { + if let Some(rest) =3D trimmed.strip_prefix("desc:") { + let text =3D rest.trim().to_string(); + if error.description.is_empty() { + error.description =3D text; + } else { + error.description.push(' '); + error.description.push_str(&text); + } + } else if let Some(rest) =3D trimmed.strip_prefix("con= dition:") { + error.condition =3D rest.trim().to_string(); + } else { + // Continuation of description + if !error.description.is_empty() { + error.description.push(' '); + error.description.push_str(trimmed); + } + } + } + } + BlockContext::Signal =3D> { + if let Some(signal) =3D current_signal.as_mut() { + if let Some(rest) =3D trimmed.strip_prefix("direction:= ") { + signal.direction =3D self.parse_signal_direction(r= est.trim()); + } else if let Some(rest) =3D trimmed.strip_prefix("act= ion:") { + signal.action =3D self.parse_signal_action(rest.tr= im()); + } else if let Some(rest) =3D trimmed.strip_prefix("con= dition:") { + signal.condition =3D Some(rest.trim().to_string()); + } else if let Some(rest) =3D trimmed.strip_prefix("des= c:") { + let text =3D rest.trim().to_string(); + if signal.description.is_none() { + signal.description =3D Some(text); + } else if let Some(d) =3D signal.description.as_mu= t() { + d.push(' '); + d.push_str(&text); + } + } else if let Some(rest) =3D trimmed.strip_prefix("err= no:") { + // `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; resol= ve + // the symbol and negate + // errno: EINTR -> bare symbol; resolved va= lue + // is already negative + let value =3D rest.trim(); + signal.error_on_signal =3D if let Ok(code) =3D val= ue.parse::() { + Some(code) + } else if let Some(name) =3D value.strip_prefix('-= ') { + // `error_name_to_code` already returns the ne= gated + // code (e.g. "EINTR" -> -4), so `-EINTR` reso= lves + // to -4 too =E2=80=94 the leading `-` on the = symbolic form + // is kernel-source convention, not a second n= egation. + Some(self.error_name_to_code(name)) + } else { + Some(self.error_name_to_code(value)) + }; + } else if let Some(rest) =3D trimmed.strip_prefix("tim= ing:") { + signal.timing =3D self.parse_signal_timing(rest.tr= im()); + } else if let Some(rest) =3D trimmed.strip_prefix("res= tartable:") { + let val =3D rest.trim().to_lowercase(); + signal.restartable =3D matches!(val.as_str(), "yes= " | "true" | "1"); + } else if let Some(rest) =3D trimmed.strip_prefix("int= erruptible:") { + let val =3D rest.trim().to_lowercase(); + signal.interruptible =3D matches!(val.as_str(), "y= es" | "true" | "1"); + } else if let Some(rest) =3D trimmed.strip_prefix("pri= ority:") { + signal.priority =3D rest.trim().parse().unwrap_or(= 0); + } else if let Some(rest) =3D trimmed.strip_prefix("tar= get:") { + signal.target =3D Some(rest.trim().to_string()); + } else if let Some(rest) =3D trimmed.strip_prefix("que= ue:") { + signal.queue =3D Some(rest.trim().to_string()); + } else if let Some(rest) =3D trimmed + .strip_prefix("transform-to:") + .or_else(|| trimmed.strip_prefix("transform_to:")) + { + // 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 + // . 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 =3D rest.trim(); + let parsed =3D v.parse::().ok(); + signal.transform_to =3D parsed; + if parsed.is_some() { + *pending_transform_warning =3D None; + } else if !v.is_empty() { + *pending_transform_warning =3D Some(v.to_strin= g()); + } + } else if let Some(rest) =3D trimmed + .strip_prefix("sa-flags-required:") + .or_else(|| trimmed.strip_prefix("sa_flags_require= d:")) + { + signal.sa_flags_required =3D self.parse_hex_or_bit= mask(rest.trim()); + } else if let Some(rest) =3D trimmed + .strip_prefix("sa-flags-forbidden:") + .or_else(|| trimmed.strip_prefix("sa_flags_forbidd= en:")) + { + signal.sa_flags_forbidden =3D self.parse_hex_or_bi= tmask(rest.trim()); + } else if let Some(rest) =3D trimmed + .strip_prefix("state-required:") + .or_else(|| trimmed.strip_prefix("state_required:"= )) + { + signal.state_required =3D self.parse_signal_state_= mask(rest.trim()); + } else if let Some(rest) =3D trimmed + .strip_prefix("state-forbidden:") + .or_else(|| trimmed.strip_prefix("state_forbidden:= ")) + { + signal.state_forbidden =3D self.parse_signal_state= _mask(rest.trim()); + } else { + // Continuation of description + if let Some(d) =3D signal.description.as_mut() { + d.push(' '); + d.push_str(trimmed); + } + } + } + } + BlockContext::Capability =3D> { + if let Some(cap) =3D current_capability.as_mut() { + if let Some(rest) =3D trimmed.strip_prefix("type:") { + cap.action =3D canon_kapi_cap_action(rest.trim()); + } else if let Some(rest) =3D trimmed.strip_prefix("all= ows:") { + cap.allows =3D rest.trim().to_string(); + } else if let Some(rest) =3D trimmed.strip_prefix("wit= hout:") { + cap.without_cap =3D rest.trim().to_string(); + } else if let Some(rest) =3D trimmed.strip_prefix("con= dition:") { + cap.check_condition =3D Some(rest.trim().to_string= ()); + } else if let Some(rest) =3D trimmed.strip_prefix("pri= ority:") { + cap.priority =3D rest.trim().parse().ok(); + } + } + } + BlockContext::SideEffect =3D> { + if let Some(effect) =3D current_side_effect.as_mut() { + if let Some(rest) =3D trimmed.strip_prefix("target:") { + effect.target =3D rest.trim().to_string(); + } else if let Some(rest) =3D trimmed.strip_prefix("con= dition:") { + effect.condition =3D Some(rest.trim().to_string()); + } else if let Some(rest) =3D trimmed.strip_prefix("des= c:") { + let text =3D rest.trim().to_string(); + if effect.description.is_empty() { + effect.description =3D text; + } else { + effect.description.push(' '); + effect.description.push_str(&text); + } + } else if let Some(rest) =3D trimmed.strip_prefix("rev= ersible:") { + let val =3D rest.trim().to_lowercase(); + effect.reversible =3D 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 =3D> { + if let Some(constraint) =3D current_constraint.as_mut() { + if let Some(rest) =3D trimmed.strip_prefix("desc:") { + let text =3D rest.trim().to_string(); + if constraint.description.is_empty() { + constraint.description =3D text; + } else { + constraint.description.push(' '); + constraint.description.push_str(&text); + } + } else if let Some(rest) =3D trimmed.strip_prefix("exp= r:") { + constraint.expression =3D Some(rest.trim().to_stri= ng()); + } else { + // Continuation of description + if !constraint.description.is_empty() { + constraint.description.push(' '); + constraint.description.push_str(trimmed); + } + } + } + } + BlockContext::Lock =3D> { + if let Some(lock) =3D current_lock.as_mut() { + if let Some(rest) =3D trimmed.strip_prefix("type:") { + lock.lock_type =3D self.parse_lock_type(rest.trim(= )); + } else if let Some(rest) =3D trimmed.strip_prefix("sco= pe:") { + lock.scope =3D match rest.trim() { + "internal" =3D> super::KAPI_LOCK_INTERNAL, + "acquires" =3D> super::KAPI_LOCK_ACQUIRES, + "releases" =3D> super::KAPI_LOCK_RELEASES, + "caller_held" =3D> super::KAPI_LOCK_CALLER_HEL= D, + _ =3D> super::KAPI_LOCK_INTERNAL, + }; + } else if let Some(rest) =3D trimmed.strip_prefix("des= c:") { + let text =3D rest.trim().to_string(); + if lock.description.is_empty() { + lock.description =3D text; + } else { + lock.description.push(' '); + lock.description.push_str(&text); + } + } else if trimmed.starts_with("acquired:") { + // KAPI_LOCK_ACQUIRED macro sets scope =3D ACQUIRE= S. + lock.scope =3D super::KAPI_LOCK_ACQUIRES; + } else if trimmed.starts_with("released:") { + // KAPI_LOCK_RELEASED macro sets scope =3D RELEASE= S, + // overriding any earlier scope. The generated + // apispec.h emits these in source order, so + // last-write-wins matches the binary layout. + lock.scope =3D super::KAPI_LOCK_RELEASES; + } else { + // Continuation of description + if !lock.description.is_empty() { + lock.description.push(' '); + lock.description.push_str(trimmed); + } + } + } + } + BlockContext::Return =3D> { + if let Some(ret) =3D current_return.as_mut() { + if let Some(rest) =3D trimmed.strip_prefix("type:") { + let raw =3D rest.trim(); + ret.type_name =3D canon_kapi_type_name(raw); + ret.return_type =3D self.parse_param_type(raw); + } else if let Some(rest) =3D trimmed.strip_prefix("che= ck-type:") { + ret.check_type =3D self.parse_return_check_type(re= st.trim()); + } else if let Some(rest) =3D trimmed.strip_prefix("suc= cess:") { + // Accepts "=3D 0", ">=3D 0", bare integer. + let val =3D rest + .trim() + .trim_start_matches(|c: char| !c.is_ascii_digi= t() && c !=3D '-'); + ret.success_value =3D val.parse().ok(); + } else if let Some(rest) =3D trimmed.strip_prefix("des= c:") { + let text =3D rest.trim().to_string(); + if ret.description.is_empty() { + ret.description =3D 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 | BlockContext::None =3D> {} + } + } + + /// 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, + current_signal: &mut Option, + current_capability: &mut Option, + current_side_effect: &mut Option, + current_constraint: &mut Option, + current_lock: &mut Option, + current_return: &mut Option, + ) { + match block { + BlockContext::Error(_) =3D> { + if let Some(error) =3D current_error.take() { + spec.errors.push(error); + } + } + BlockContext::Signal =3D> { + if let Some(signal) =3D current_signal.take() { + spec.signals.push(signal); + } + } + BlockContext::Capability =3D> { + if let Some(cap) =3D current_capability.take() { + spec.capabilities.push(cap); + } + } + BlockContext::SideEffect =3D> { + if let Some(effect) =3D current_side_effect.take() { + spec.side_effects.push(effect); + } + } + BlockContext::Constraint =3D> { + if let Some(constraint) =3D current_constraint.take() { + spec.constraints.push(constraint); + } + } + BlockContext::Lock =3D> { + if let Some(lock) =3D current_lock.take() { + spec.locks.push(lock); + } + } + BlockContext::Return =3D> { + if let Some(ret) =3D current_return.take() { + spec.return_spec =3D Some(ret); + } + } + _ =3D> {} + } + *block =3D BlockContext::None; + } + + /// Extract parameter type names from SYSCALL_DEFINE signature + fn extract_types_from_signature(&self, sig: &str) -> HashMap { + let mut types =3D HashMap::new(); + + // Find content between outermost parens + let content =3D if let Some(start) =3D sig.find('(') { + let end =3D 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, name= 2, ...) + let parts: Vec<&str> =3D content.split(',').map(|s| s.trim()).coll= ect(); + + // Skip first part (syscall name), then process pairs + let mut i =3D 1; + while i + 1 < parts.len() { + let type_part =3D parts[i].trim(); + let name_part =3D parts[i + 1].trim(); + + // Build the type_name string: "type name" + let type_name =3D format!("{} {}", type_part, name_part); + types.insert(name_part.to_string(), type_name); + + i +=3D 2; + } + + types + } + + fn collect_multiline_value( + &self, + lines: &[&str], + start_idx: usize, + first_part: &str, + ) -> (String, usize) { + let mut result =3D String::from(first_part.trim()); + let mut i =3D start_idx + 1; + + while i < lines.len() { + let line =3D lines[i]; + + if self.is_annotation_line(line) { + break; + } + + if !line.trim().is_empty() && line.starts_with(" ") { + if !result.is_empty() { + result.push(' '); + } + result.push_str(line.trim()); + } else if line.trim().is_empty() { + i +=3D 1; + continue; + } else { + break; + } + + i +=3D 1; + } + + (result, i) + } + + fn is_annotation_line(&self, line: &str) -> bool { + let trimmed =3D line.trim_start(); + if !trimmed.contains(':') { + return false; + } + let annotations =3D [ + "param:", + "param-", + "error:", + "error-", + "lock:", + "lock-", + "signal:", + "signal-", + "side-effect:", + "state-trans:", + "capability:", + "capability-", + "constraint:", + "constraint-", + "struct-", + "return:", + "return-", + "examples:", + "notes:", + "since-", + "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 =3D text.trim(); + if t.is_empty() { + return false; + } + // Split NAME ( ARGS ) =E2=80=94 no nesting, no escaping. + let (name, args): (&str, Option<&str>) =3D match (t.find('('), t.r= find(')')) { + (Some(lp), Some(rp)) if rp > lp =3D> (t[..lp].trim(), Some(t[l= p + 1..rp].trim())), + _ =3D> (t, None), + }; + // Bail out on anything that looks like free text (spaces inside t= he + // name part) so we don't swallow existing textual constraints. + if name.contains(char::is_whitespace) || name.is_empty() { + return false; + } + let name_lc =3D name.to_ascii_lowercase(); + let split_args =3D || -> Vec { + args.map(|a| a.split(',').map(|s| s.trim().to_string()).collec= t()) + .unwrap_or_default() + }; + match name_lc.as_str() { + "range" =3D> { + let a =3D split_args(); + if a.len() !=3D 2 { + return false; + } + param.min_value =3D a[0].parse().ok(); + param.max_value =3D a[1].parse().ok(); + param.constraint_type =3D 1; // KAPI_CONSTRAINT_RANGE + true + } + "mask" =3D> { + let a =3D split_args(); + if a.len() !=3D 1 { + return false; + } + // Symbolic masks (e.g. "O_RDONLY | O_WRONLY | ...") can't + // be resolved at parse time =E2=80=94 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 =3D parse_u64_literal(&a[0]); + param.constraint_type =3D 2; // KAPI_CONSTRAINT_MASK + true + } + "enum" =3D> { + let a =3D split_args(); + if a.is_empty() { + return false; + } + param.enum_values =3D a; + param.constraint_type =3D 3; // KAPI_CONSTRAINT_ENUM + true + } + "alignment" | "align" =3D> { + let a =3D split_args(); + if a.len() !=3D 1 { + return false; + } + param.alignment =3D a[0].parse().ok(); + param.constraint_type =3D 4; // KAPI_CONSTRAINT_ALIGNMENT + true + } + "power_of_two" =3D> { + if args.is_some() { + return false; + } + param.constraint_type =3D 5; // KAPI_CONSTRAINT_POWER_OF_T= WO + true + } + "page_aligned" =3D> { + if args.is_some() { + return false; + } + param.constraint_type =3D 6; // KAPI_CONSTRAINT_PAGE_ALIGN= ED + true + } + "nonzero" =3D> { + if args.is_some() { + return false; + } + param.constraint_type =3D 7; // KAPI_CONSTRAINT_NONZERO + true + } + "user_string" =3D> { + // Optional size argument: user_string(N) + if let Some(arg) =3D args { + if let Ok(n) =3D arg.trim().parse::() { + param.size =3D Some(n); + } + } + param.constraint_type =3D 8; // KAPI_CONSTRAINT_USER_STRING + true + } + "user_path" =3D> { + if args.is_some() { + return false; + } + param.constraint_type =3D 9; // KAPI_CONSTRAINT_USER_PATH + true + } + "user_ptr" =3D> { + if args.is_some() { + return false; + } + param.constraint_type =3D 10; // KAPI_CONSTRAINT_USER_PTR + true + } + "buffer" =3D> { + // buffer(size_param_idx) =E2=80=94 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 =3D split_args(); + if a.len() !=3D 1 { + return false; + } + param.size_param_idx =3D a[0].parse().ok(); + param.constraint_type =3D 11; // KAPI_CONSTRAINT_BUFFER + true + } + "custom" =3D> { + // custom(fn_name) =E2=80=94 record function name as free-= text constraint + // so downstream tooling can wire it up. + if let Some(arg) =3D args { + param.constraint =3D Some(arg.trim().to_string()); + } + param.constraint_type =3D 12; // KAPI_CONSTRAINT_CUSTOM + true + } + _ =3D> false, + } + } + + fn parse_context_flags(&self, flags: &str) -> Vec { + 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_SLEEP= ABLE"]). + /// Tokens that already look like KAPI_CTX_* are passed through. + fn parse_context_list(&self, flags: &str) -> Vec { + 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 =3D tok.trim(); + if t.is_empty() { + return String::new(); + } + match t.to_ascii_lowercase().as_str() { + "process" =3D> "KAPI_CTX_PROCESS".to_string(), + "softirq" =3D> "KAPI_CTX_SOFTIRQ".to_string(), + "hardirq" =3D> "KAPI_CTX_HARDIRQ".to_string(), + "nmi" =3D> "KAPI_CTX_NMI".to_string(), + "atomic" =3D> "KAPI_CTX_ATOMIC".to_string(), + "sleepable" =3D> "KAPI_CTX_SLEEPABLE".to_string(), + "preempt_disabled" =3D> "KAPI_CTX_PREEMPT_DISABLED".to_string(= ), + "irq_disabled" =3D> "KAPI_CTX_IRQ_DISABLED".to_string(), + _ =3D> t.to_string(), + } + } + + fn error_name_to_code(&self, name: &str) -> i32 { + match name { + "EPERM" =3D> -1, + "ENOENT" =3D> -2, + "ESRCH" =3D> -3, + "EINTR" =3D> -4, + "EIO" =3D> -5, + "ENXIO" =3D> -6, + "E2BIG" =3D> -7, + "ENOEXEC" =3D> -8, + "EBADF" =3D> -9, + "ECHILD" =3D> -10, + "EAGAIN" | "EWOULDBLOCK" =3D> -11, + "ENOMEM" =3D> -12, + "EACCES" =3D> -13, + "EFAULT" =3D> -14, + "ENOTBLK" =3D> -15, + "EBUSY" =3D> -16, + "EEXIST" =3D> -17, + "EXDEV" =3D> -18, + "ENODEV" =3D> -19, + "ENOTDIR" =3D> -20, + "EISDIR" =3D> -21, + "EINVAL" =3D> -22, + "ENFILE" =3D> -23, + "EMFILE" =3D> -24, + "ENOTTY" =3D> -25, + "ETXTBSY" =3D> -26, + "EFBIG" =3D> -27, + "ENOSPC" =3D> -28, + "ESPIPE" =3D> -29, + "EROFS" =3D> -30, + "EMLINK" =3D> -31, + "EPIPE" =3D> -32, + "EDOM" =3D> -33, + "ERANGE" =3D> -34, + "EDEADLK" =3D> -35, + "ENAMETOOLONG" =3D> -36, + "ENOLCK" =3D> -37, + "ENOSYS" =3D> -38, + "ENOTEMPTY" =3D> -39, + "ELOOP" =3D> -40, + "ENOMSG" =3D> -42, + "ENODATA" =3D> -61, + "ENOLINK" =3D> -67, + "EPROTO" =3D> -71, + "EOVERFLOW" =3D> -75, + "ELIBBAD" =3D> -80, + "EILSEQ" =3D> -84, + "ENOTSOCK" =3D> -88, + "EDESTADDRREQ" =3D> -89, + "EMSGSIZE" =3D> -90, + "EPROTOTYPE" =3D> -91, + "ENOPROTOOPT" =3D> -92, + "EPROTONOSUPPORT" =3D> -93, + "EOPNOTSUPP" | "ENOTSUP" =3D> -95, + "EADDRINUSE" =3D> -98, + "EADDRNOTAVAIL" =3D> -99, + "ENETDOWN" =3D> -100, + "ENETUNREACH" =3D> -101, + "ENETRESET" =3D> -102, + "ECONNABORTED" =3D> -103, + "ECONNRESET" =3D> -104, + "ENOBUFS" =3D> -105, + "EISCONN" =3D> -106, + "ENOTCONN" =3D> -107, + "ETIMEDOUT" =3D> -110, + "ECONNREFUSED" =3D> -111, + "EALREADY" =3D> -114, + "EINPROGRESS" =3D> -115, + "ESTALE" =3D> -116, + "EDQUOT" =3D> -122, + "ENOMEDIUM" =3D> -123, + "ENOKEY" =3D> -126, + "ERESTARTSYS" =3D> -512, + _ =3D> 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 =3D type_str.trim(); + match s { + "KAPI_TYPE_VOID" =3D> 0, + "KAPI_TYPE_INT" =3D> 1, + "KAPI_TYPE_UINT" =3D> 2, + "KAPI_TYPE_PTR" =3D> 3, + "KAPI_TYPE_STRUCT" =3D> 4, + "KAPI_TYPE_UNION" =3D> 5, + "KAPI_TYPE_ENUM" =3D> 6, + "KAPI_TYPE_FUNC_PTR" =3D> 7, + "KAPI_TYPE_ARRAY" =3D> 8, + "KAPI_TYPE_FD" =3D> 9, + "KAPI_TYPE_USER_PTR" =3D> 10, + "KAPI_TYPE_PATH" =3D> 11, + "KAPI_TYPE_CUSTOM" =3D> 12, + _ =3D> match s.to_ascii_lowercase().as_str() { + "void" =3D> 0, + "int" =3D> 1, + "uint" =3D> 2, + "ptr" =3D> 3, + "struct" =3D> 4, + "union" =3D> 5, + "enum" =3D> 6, + "func_ptr" =3D> 7, + "array" =3D> 8, + "fd" =3D> 9, + "user_ptr" | "uptr" =3D> 10, + "path" =3D> 11, + "custom" =3D> 12, + _ =3D> 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 =3D type_str.trim(); + match s { + "KAPI_CONSTRAINT_NONE" =3D> 0, + "KAPI_CONSTRAINT_RANGE" =3D> 1, + "KAPI_CONSTRAINT_MASK" =3D> 2, + "KAPI_CONSTRAINT_ENUM" =3D> 3, + "KAPI_CONSTRAINT_ALIGNMENT" =3D> 4, + "KAPI_CONSTRAINT_POWER_OF_TWO" =3D> 5, + "KAPI_CONSTRAINT_PAGE_ALIGNED" =3D> 6, + "KAPI_CONSTRAINT_NONZERO" =3D> 7, + "KAPI_CONSTRAINT_USER_STRING" =3D> 8, + "KAPI_CONSTRAINT_USER_PATH" =3D> 9, + "KAPI_CONSTRAINT_USER_PTR" =3D> 10, + "KAPI_CONSTRAINT_BUFFER" =3D> 11, + "KAPI_CONSTRAINT_CUSTOM" =3D> 12, + _ =3D> 0, + } + } + + fn parse_field_type(&self, type_str: &str) -> u32 { + match type_str { + "__s32" | "int" =3D> 1, + "__u32" | "unsigned int" =3D> 2, + "__s64" | "long" =3D> 3, + "__u64" | "unsigned long" =3D> 4, + _ =3D> 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 =3D tok.trim(); + // Long / existing short forms first. + match t { + "KAPI_PARAM_IN" | "IN" =3D> return 1, + "KAPI_PARAM_OUT" | "OUT" =3D> return 2, + "KAPI_PARAM_INOUT" | "INOUT" =3D> return 3, + "KAPI_PARAM_OPTIONAL" | "OPTIONAL" =3D> return 1 << 3, + "KAPI_PARAM_CONST" | "CONST" =3D> return 1 << 4, + "KAPI_PARAM_VOLATILE" | "VOLATILE" =3D> return 1 << 5, + "KAPI_PARAM_USER" | "USER" =3D> return 1 << 6, + "KAPI_PARAM_DMA" | "DMA" =3D> return 1 << 7, + "KAPI_PARAM_ALIGNED" | "ALIGNED" =3D> return 1 << 8, + _ =3D> {} + } + // English short aliases (case-insensitive). + match t.to_ascii_lowercase().as_str() { + "input" =3D> 1, + "output" =3D> 2, + "inout" =3D> 3, + "optional" =3D> 1 << 3, + "const" =3D> 1 << 4, + "volatile" =3D> 1 << 5, + "user" =3D> 1 << 6, + "dma" =3D> 1 << 7, + "aligned" =3D> 1 << 8, + _ =3D> 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 =3D type_str.trim(); + match s { + "KAPI_LOCK_NONE" =3D> 0, + "KAPI_LOCK_MUTEX" =3D> 1, + "KAPI_LOCK_SPINLOCK" =3D> 2, + "KAPI_LOCK_RWLOCK" =3D> 3, + "KAPI_LOCK_SEQLOCK" =3D> 4, + "KAPI_LOCK_RCU" =3D> 5, + "KAPI_LOCK_SEMAPHORE" =3D> 6, + "KAPI_LOCK_CUSTOM" =3D> 7, + _ =3D> match s.to_ascii_lowercase().as_str() { + "none" =3D> 0, + "mutex" =3D> 1, + "spinlock" =3D> 2, + "rwlock" =3D> 3, + "seqlock" =3D> 4, + "rcu" =3D> 5, + "semaphore" =3D> 6, + "custom" =3D> 7, + _ =3D> 0, + }, + } + } + + fn parse_signal_direction(&self, dir: &str) -> u32 { + let s =3D dir.trim(); + match s { + "KAPI_SIGNAL_RECEIVE" =3D> 1, + "KAPI_SIGNAL_SEND" =3D> 2, + "KAPI_SIGNAL_HANDLE" =3D> 4, + "KAPI_SIGNAL_BLOCK" =3D> 8, + "KAPI_SIGNAL_IGNORE" =3D> 16, + _ =3D> match s.to_ascii_lowercase().as_str() { + "receive" =3D> 1, + "send" =3D> 2, + "handle" =3D> 4, + "block" =3D> 8, + "ignore" =3D> 16, + _ =3D> 0, + }, + } + } + + fn parse_signal_action(&self, action: &str) -> u32 { + let s =3D action.trim(); + match s { + "KAPI_SIGNAL_ACTION_DEFAULT" =3D> 0, + "KAPI_SIGNAL_ACTION_TERMINATE" =3D> 1, + "KAPI_SIGNAL_ACTION_COREDUMP" =3D> 2, + "KAPI_SIGNAL_ACTION_STOP" =3D> 3, + "KAPI_SIGNAL_ACTION_CONTINUE" =3D> 4, + "KAPI_SIGNAL_ACTION_CUSTOM" =3D> 5, + "KAPI_SIGNAL_ACTION_RETURN" =3D> 6, + "KAPI_SIGNAL_ACTION_RESTART" =3D> 7, + "KAPI_SIGNAL_ACTION_QUEUE" =3D> 8, + "KAPI_SIGNAL_ACTION_DISCARD" =3D> 9, + "KAPI_SIGNAL_ACTION_TRANSFORM" =3D> 10, + _ =3D> match s.to_ascii_lowercase().as_str() { + "default" =3D> 0, + "terminate" =3D> 1, + "coredump" =3D> 2, + "stop" =3D> 3, + "continue" =3D> 4, + "custom" =3D> 5, + "return" =3D> 6, + "restart" =3D> 7, + "queue" =3D> 8, + "discard" =3D> 9, + "transform" =3D> 10, + _ =3D> 0, + }, + } + } + + fn parse_signal_timing(&self, timing: &str) -> u32 { + let s =3D timing.trim(); + match s { + "KAPI_SIGNAL_TIME_BEFORE" =3D> 0, + "KAPI_SIGNAL_TIME_DURING" =3D> 1, + "KAPI_SIGNAL_TIME_AFTER" =3D> 2, + _ =3D> match s.to_ascii_lowercase().as_str() { + "before" =3D> 0, + "during" =3D> 1, + "after" =3D> 2, + _ =3D> 0, + }, + } + } + + fn parse_signal_state(&self, state: &str) -> u32 { + match state { + "KAPI_SIGNAL_STATE_RUNNING" =3D> 1, + "KAPI_SIGNAL_STATE_SLEEPING" =3D> 2, + _ =3D> 0, + } + } + + /// Accept a hex literal ("0x4"), a decimal literal ("4"), or a '|'-se= parated + /// bitmask expression. Unknown tokens contribute 0. + fn parse_hex_or_bitmask(&self, value: &str) -> u32 { + let v =3D value.trim(); + if let Some(hex) =3D v.strip_prefix("0x").or_else(|| v.strip_prefi= x("0X")) { + if let Ok(n) =3D u32::from_str_radix(hex, 16) { + return n; + } + } + if let Ok(n) =3D v.parse::() { + return n; + } + let mut acc =3D 0u32; + for part in v.split(['|', ',']) { + let t =3D part.trim(); + if t.is_empty() { + continue; + } + if let Some(hex) =3D t.strip_prefix("0x").or_else(|| t.strip_p= refix("0X")) { + if let Ok(n) =3D u32::from_str_radix(hex, 16) { + acc |=3D n; + continue; + } + } + if let Ok(n) =3D t.parse::() { + acc |=3D 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 =3D 0u32; + for part in value.split(['|', ',']) { + let t =3D part.trim().trim_start_matches("KAPI_SIGNAL_STATE_"); + let bit =3D match t.to_ascii_uppercase().as_str() { + "RUNNING" =3D> 1 << 0, + "SLEEPING" =3D> 1 << 1, + "STOPPED" =3D> 1 << 2, + "TRACED" =3D> 1 << 3, + "ZOMBIE" =3D> 1 << 4, + "DEAD" =3D> 1 << 5, + _ =3D> 0, + }; + acc |=3D 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 =3D if type_str.contains('|') || !type_str.contains(',') { + '|' + } else { + ',' + }; + let mut result =3D 0; + for flag in type_str.split(sep) { + let t =3D flag.trim(); + let bit =3D match t { + "KAPI_EFFECT_NONE" =3D> 0, + "KAPI_EFFECT_ALLOC_MEMORY" =3D> 1 << 0, + "KAPI_EFFECT_FREE_MEMORY" =3D> 1 << 1, + "KAPI_EFFECT_MODIFY_STATE" =3D> 1 << 2, + "KAPI_EFFECT_SIGNAL_SEND" =3D> 1 << 3, + "KAPI_EFFECT_FILE_POSITION" =3D> 1 << 4, + "KAPI_EFFECT_LOCK_ACQUIRE" =3D> 1 << 5, + "KAPI_EFFECT_LOCK_RELEASE" =3D> 1 << 6, + "KAPI_EFFECT_RESOURCE_CREATE" =3D> 1 << 7, + "KAPI_EFFECT_RESOURCE_DESTROY" =3D> 1 << 8, + "KAPI_EFFECT_SCHEDULE" =3D> 1 << 9, + "KAPI_EFFECT_HARDWARE" =3D> 1 << 10, + "KAPI_EFFECT_NETWORK" =3D> 1 << 11, + "KAPI_EFFECT_FILESYSTEM" =3D> 1 << 12, + "KAPI_EFFECT_PROCESS_STATE" =3D> 1 << 13, + "KAPI_EFFECT_IRREVERSIBLE" =3D> 1 << 14, + _ =3D> match t.to_ascii_lowercase().as_str() { + "none" =3D> 0, + "alloc_memory" =3D> 1 << 0, + "free_memory" =3D> 1 << 1, + "modify_state" =3D> 1 << 2, + "signal_send" =3D> 1 << 3, + "file_position" =3D> 1 << 4, + "lock_acquire" =3D> 1 << 5, + "lock_release" =3D> 1 << 6, + "resource_create" =3D> 1 << 7, + "resource_destroy" =3D> 1 << 8, + "schedule" =3D> 1 << 9, + "hardware" =3D> 1 << 10, + "network" =3D> 1 << 11, + "filesystem" =3D> 1 << 12, + "process_state" =3D> 1 << 13, + "irreversible" =3D> 1 << 14, + _ =3D> 0, + }, + }; + result |=3D bit; + } + result + } + + fn parse_capability_value(&self, cap: &str) -> i32 { + match cap { + "CAP_CHOWN" =3D> 0, + "CAP_DAC_OVERRIDE" =3D> 1, + "CAP_DAC_READ_SEARCH" =3D> 2, + "CAP_FOWNER" =3D> 3, + "CAP_FSETID" =3D> 4, + "CAP_KILL" =3D> 5, + "CAP_SETGID" =3D> 6, + "CAP_SETUID" =3D> 7, + "CAP_SETPCAP" =3D> 8, + "CAP_LINUX_IMMUTABLE" =3D> 9, + "CAP_NET_BIND_SERVICE" =3D> 10, + "CAP_NET_BROADCAST" =3D> 11, + "CAP_NET_ADMIN" =3D> 12, + "CAP_NET_RAW" =3D> 13, + "CAP_IPC_LOCK" =3D> 14, + "CAP_IPC_OWNER" =3D> 15, + "CAP_SYS_MODULE" =3D> 16, + "CAP_SYS_RAWIO" =3D> 17, + "CAP_SYS_CHROOT" =3D> 18, + "CAP_SYS_PTRACE" =3D> 19, + "CAP_SYS_PACCT" =3D> 20, + "CAP_SYS_ADMIN" =3D> 21, + "CAP_SYS_BOOT" =3D> 22, + "CAP_SYS_NICE" =3D> 23, + "CAP_SYS_RESOURCE" =3D> 24, + "CAP_SYS_TIME" =3D> 25, + "CAP_SYS_TTY_CONFIG" =3D> 26, + "CAP_MKNOD" =3D> 27, + "CAP_LEASE" =3D> 28, + "CAP_AUDIT_WRITE" =3D> 29, + "CAP_AUDIT_CONTROL" =3D> 30, + "CAP_SETFCAP" =3D> 31, + "CAP_MAC_OVERRIDE" =3D> 32, + "CAP_MAC_ADMIN" =3D> 33, + "CAP_SYSLOG" =3D> 34, + "CAP_WAKE_ALARM" =3D> 35, + "CAP_BLOCK_SUSPEND" =3D> 36, + "CAP_AUDIT_READ" =3D> 37, + "CAP_PERFMON" =3D> 38, + "CAP_BPF" =3D> 39, + "CAP_CHECKPOINT_RESTORE" =3D> 40, + _ =3D> 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 =3D check.trim(); + match s { + "KAPI_RETURN_EXACT" =3D> 0, + "KAPI_RETURN_RANGE" =3D> 1, + "KAPI_RETURN_ERROR_CHECK" =3D> 2, + "KAPI_RETURN_FD" =3D> 3, + "KAPI_RETURN_CUSTOM" =3D> 4, + "KAPI_RETURN_NO_RETURN" =3D> 5, + _ =3D> match s.to_ascii_lowercase().as_str() { + "exact" =3D> 0, + "range" =3D> 1, + "error_check" =3D> 2, + "fd" =3D> 3, + "custom" =3D> 4, + "no_return" =3D> 5, + _ =3D> 0, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parser() -> KerneldocParserImpl { + KerneldocParserImpl::new() + } + + #[test] + fn parse_minimal_kerneldoc() { + let doc =3D "\ +sys_foo - Do something useful +context-flags: KAPI_CTX_PROCESS +param-count: 1 +@fd: The file descriptor +param-type: fd, KAPI_TYPE_INT +error: EBADF, Bad file descriptor +"; + let spec =3D 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.parameters[0].param_type, 1); // KAPI_TYPE_INT + 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 =3D "\ +sys_bar - Multiple params +@fd: file descriptor arg +@buf: user buffer +@count: byte count +@flags: option flags +param-type: fd, KAPI_TYPE_FD +param-type: buf, KAPI_TYPE_USER_PTR +param-type: count, KAPI_TYPE_UINT +param-type: flags, KAPI_TYPE_UINT +"; + let sig =3D "(bar, int, fd, char __user *, buf, size_t, count, uns= igned long, flags)"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_bar", "syscall", Some(sig)) + .unwrap(); + + assert_eq!(spec.parameters.len(), 4); + + let fd_param =3D spec.parameters.iter().find(|p| p.name =3D=3D "fd= ").unwrap(); + assert_eq!(fd_param.param_type, 9); // FD (kernel enum) + + let buf_param =3D spec.parameters.iter().find(|p| p.name =3D=3D "b= uf").unwrap(); + assert_eq!(buf_param.param_type, 10); // USER_PTR (kernel enum) + assert_eq!(buf_param.type_name, "char __user * buf"); + + let count_param =3D spec.parameters.iter().find(|p| p.name =3D=3D = "count").unwrap(); + assert_eq!(count_param.param_type, 2); // UINT + + let flags_param =3D spec.parameters.iter().find(|p| p.name =3D=3D = "flags").unwrap(); + assert_eq!(flags_param.param_type, 2); // UINT + } + + #[test] + fn parse_error_codes_with_descriptions() { + let doc =3D "\ +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 =3D 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 =3D "\ +sys_ctx - Context test +context-flags: KAPI_CTX_PROCESS|KAPI_CTX_SLEEPABLE +"; + let spec =3D 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 =3D "\ +sys_ctx - Context test +contexts: process, sleepable +"; + let spec =3D 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 =3D "\ +sys_ctx - Context test +contexts: process, KAPI_CTX_SLEEPABLE, softirq +"; + let spec =3D 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:" still accepts "|"-joined short + // aliases so mid-migration files parse correctly. + let doc =3D "\ +sys_ctx - Context test +context-flags: process | KAPI_CTX_SLEEPABLE +"; + let spec =3D 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 =3D "\ +sys_t - Short type test +param: size + type: uint, input +"; + let spec =3D 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 =3D "\ +sys_t - Short type test +param: filename + type: path, input, user +"; + let spec =3D 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 enu= m) + 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 =3D "\ +sys_c - Constraint test +param: count + type: uint, input + constraint-type: range(0, 4096) +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_c", "syscall", None) + .unwrap(); + + let p =3D &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 =3D "\ +sys_c - Constraint test +param: flags + type: uint, input + constraint-type: mask(0xff) +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_c", "syscall", None) + .unwrap(); + + let p =3D &spec.parameters[0]; + assert_eq!(p.constraint_type, 2); // KAPI_CONSTRAINT_MASK + assert_eq!(p.valid_mask, Some(0xff)); + } + + #[test] + fn user_ptr_type_implies_user_flag() { + let doc =3D "\ +sys_u - Implicit user flag test +param: buf + type: user_ptr, output +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_u", "syscall", None) + .unwrap(); + + let p =3D &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 =3D "\ +sys_fd - fd has no implicit user flag +param: fd + type: fd, input +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_fd", "syscall", None) + .unwrap(); + + let p =3D &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 =3D "\ +sys_p - Path implicit user flag +param: filename + type: path, input +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_p", "syscall", None) + .unwrap(); + + let p =3D &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 =3D "\ +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 =3D "\ +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 sp_l =3D parser() + .parse_kerneldoc(long, "sys_x", "syscall", None) + .unwrap(); + let sp_s =3D parser() + .parse_kerneldoc(short, "sys_x", "syscall", None) + .unwrap(); + 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 =3D "\ +sys_b - Buffer test +param: buf + type: user_ptr, output, user + constraint-type: buffer(2) +"; + let spec =3D 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 =3D "\ +sys_b - Symmetric buffer test +param: buf + type: user_ptr, output, user + constraint-type: buffer(2) +"; + let long =3D "\ +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 =3D parser() + .parse_kerneldoc(short, "sys_b", "syscall", None) + .unwrap(); + let sp_l =3D 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 =3D "\ +sys_c - Constraint test +param: filename + type: path, input, user + constraint-type: user_path +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_c", "syscall", None) + .unwrap(); + + assert_eq!(spec.parameters[0].constraint_type, 9); // USER_PATH + } + + #[test] + fn is_block_key_recognises_ident_colon() { + // Any bare `IDENT:` indented line must end a continuation fold. + assert!(super::is_block_key("type:")); + assert!(super::is_block_key("constraint-type:")); + assert!(super::is_block_key("valid-mask: 0xff")); + assert!(super::is_block_key("error: -EINTR")); + assert!(super::is_block_key("expr: some expression")); + assert!(super::is_block_key("reversible: yes")); + // Expression fragments and punctuation are not block keys. + assert!(!super::is_block_key("O_RDONLY | O_WRONLY |")); + assert!(!super::is_block_key(")")); + assert!(!super::is_block_key("Must be positive.")); + } + + #[test] + fn multiline_fold_stops_at_sibling_block_attribute() { + // A signal: block below a param: block. The constraint-type's + // continuation must not greedily eat the next signal block's + // `direction:` or the final `error:` line. (Kerneldoc section + // headers are at indent 0, which the top-level fold check stops + // on anyway; this test asserts that sibling *indented* keys + // also stop the fold.) + let doc =3D "\ +sys_y - Fold stop test +param: f + type: int, input + constraint-type: mask(FOO | + BAR) + cdesc: something about f + direction: KAPI_SIGNAL_RECEIVE +"; + let spec =3D 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); + } + + #[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 =3D "\ +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 =3D parser() + .parse_kerneldoc(doc, "sys_x", "syscall", None) + .unwrap(); + + assert_eq!(spec.parameters.len(), 1); + let p =3D &spec.parameters[0]; + assert_eq!(p.constraint_type, 2, "multi-line mask must set MASK ty= pe"); + // Symbolic mask =E2=80=94 must stay unresolved rather than becomi= ng Some(0). + assert_eq!( + p.valid_mask, None, + "symbolic mask values must remain None, not Some(0)" + ); + } + + #[test] + fn parse_constraint_long_form_still_works() { + let doc =3D "\ +sys_c - Constraint test +param: foo + type: uint, input + constraint-type: KAPI_CONSTRAINT_MASK + valid-mask: 0xff +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_c", "syscall", None) + .unwrap(); + + let p =3D &spec.parameters[0]; + assert_eq!(p.constraint_type, 2); // KAPI_CONSTRAINT_MASK + } + + #[test] + fn parse_constraint_free_text_still_works() { + // `constraint:` carries free-text constraint description; + // function-call short form lives on `constraint-type:`. + let doc =3D "\ +sys_c - Constraint test +param: foo + type: uint, input + constraint: must be a valid page descriptor +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_c", "syscall", None) + .unwrap(); + + let p =3D &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 =3D "\ +sys_d - Description alias test +@size: kerneldoc short description +param: size + type: uint, input + description: The new long form description. +"; + let spec =3D 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() { + // The regression test for the DSL cleanup: two spellings of the + // same spec must produce identical ApiSpec JSON. + let long =3D "\ +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 =3D "\ +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 =3D parser() + .parse_kerneldoc(long, "sys_open", "syscall", None) + .unwrap(); + let short_spec =3D parser() + .parse_kerneldoc(short, "sys_open", "syscall", None) + .unwrap(); + + // ApiSpec isn't Serialize as a whole, so compare the Debug + // rendering =E2=80=94 that still proves every field canonicalises + // identically. + let d_long =3D format!("{:#?}", long_spec); + let d_short =3D 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 =3D "\ +sys_cap - Capability test +capability: CAP_SYS_ADMIN + type: required + allows: Full system administration + without: Operation not permitted + condition: always + priority: 5 +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_cap", "syscall", None) + .unwrap(); + + assert_eq!(spec.capabilities.len(), 1); + let cap =3D &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 =3D "\ +sys_lock - Lock test +lock: files_lock, KAPI_LOCK_MUTEX + scope: acquires + desc: Protects file table +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_lock", "syscall", None) + .unwrap(); + + assert_eq!(spec.locks.len(), 1); + let lock =3D &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_signal_block() { + let doc =3D "\ +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 =3D parser() + .parse_kerneldoc(doc, "sys_sig", "syscall", None) + .unwrap(); + + assert_eq!(spec.signals.len(), 1); + let sig =3D &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 =3D format!( + "sys_s - Signal errno test\n\ + signal: SIGINT\n\ + \x20 direction: receive\n\ + \x20 action: return\n\ + \x20 {}\n", + form, + ); + let spec =3D 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 =3D "\ +sys_se - Side effect test +side-effect: KAPI_EFFECT_MODIFY_STATE, file_table, Allocates a new file de= scriptor +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_se", "syscall", None) + .unwrap(); + + assert_eq!(spec.side_effects.len(), 1); + let se =3D &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 =3D "\ +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 =3D parser() + .parse_kerneldoc(doc, "sys_se2", "syscall", None) + .unwrap(); + + assert_eq!(spec.side_effects.len(), 1); + let se =3D &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 =3D ""; + let spec =3D 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 =3D "\ +sys_simple - Just a simple syscall +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_simple", "syscall", None) + .unwrap(); + + assert_eq!(spec.description.as_deref(), Some("Just a simple syscal= l")); + assert!(spec.parameters.is_empty()); + assert!(spec.errors.is_empty()); + assert!(spec.context_flags.is_empty()); + } + + #[test] + fn parse_constraint_block() { + let doc =3D "\ +sys_cst - Constraint test +constraint: valid_fd + desc: File descriptor must be valid and open + expr: fd >=3D 0 && fd < NR_OPEN +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_cst", "syscall", None) + .unwrap(); + + assert_eq!(spec.constraints.len(), 1); + let cst =3D &spec.constraints[0]; + assert_eq!(cst.name, "valid_fd"); + assert_eq!(cst.description, "File descriptor must be valid and ope= n"); + assert_eq!(cst.expression.as_deref(), Some("fd >=3D 0 && fd < NR_O= PEN")); + } + + #[test] + fn parse_state_transition_flat() { + let doc =3D "\ +sys_st - State transition test +state-trans: fd, open, closed, File descriptor is closed +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_st", "syscall", None) + .unwrap(); + + assert_eq!(spec.state_transitions.len(), 1); + let st =3D &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 =3D "\ +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 =3D parser() + .parse_kerneldoc(doc, "sys_rng", "syscall", None) + .unwrap(); + + assert_eq!(spec.parameters.len(), 1); + let p =3D &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 =3D "\ +sys_ret - Return test +return: + type: KAPI_TYPE_INT + check-type: KAPI_RETURN_FD + success: 0 + desc: Returns file descriptor on success +"; + let spec =3D parser() + .parse_kerneldoc(doc, "sys_ret", "syscall", None) + .unwrap(); + + let ret =3D 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, Some(0)); + assert_eq!(ret.description, "Returns file descriptor on success"); + } +} diff --git a/tools/kapi/src/extractor/mod.rs b/tools/kapi/src/extractor/mod= .rs new file mode 100644 index 0000000000000..2d08dbd8769f8 --- /dev/null +++ b/tools/kapi/src/extractor/mod.rs @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +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, + pub priority: Option, + pub alternatives: Vec, +} + +/// 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, + pub min_value: Option, + pub max_value: Option, + pub valid_mask: Option, + pub enum_values: Vec, + pub size: Option, + pub alignment: Option, + /// 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, +} + +/// 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, + pub success_min: Option, + pub success_max: Option, + pub error_values: Vec, +} + +/// 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, + pub condition: Option, + pub description: Option, + pub timing: u32, + pub priority: u32, + pub restartable: bool, + pub interruptible: bool, + pub queue: Option, + 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, + /// Signal number to transform to (e.g. `SIGKILL` =E2=86=92 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, +} + +/// Signal mask specification +#[derive(Debug, Clone, serde::Serialize)] +pub struct SignalMaskSpec { + pub name: String, + pub description: String, +} + +/// Side effect specification +#[derive(Debug, Clone, serde::Serialize)] +pub struct SideEffectSpec { + pub effect_type: u32, + pub target: String, + pub condition: Option, + 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, + pub description: String, +} + +/// Constraint specification +#[derive(Debug, Clone, serde::Serialize)] +pub struct ConstraintSpec { + pub name: String, + pub description: String, + pub expression: Option, +} + +/// Lock scope enum values matching kernel enum kapi_lock_scope +pub const KAPI_LOCK_INTERNAL: u32 =3D 0; +pub const KAPI_LOCK_ACQUIRES: u32 =3D 1; +pub const KAPI_LOCK_RELEASES: u32 =3D 2; +pub const KAPI_LOCK_CALLER_HELD: u32 =3D 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, + 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, + pub long_description: Option, + pub version: Option, + pub context_flags: Vec, + pub param_count: Option, + pub error_count: Option, + pub examples: Option, + pub notes: Option, + // Sysfs-specific fields + pub subsystem: Option, + pub sysfs_path: Option, + pub permissions: Option, + pub capabilities: Vec, + pub parameters: Vec, + pub return_spec: Option, + pub errors: Vec, + pub signals: Vec, + pub signal_masks: Vec, + pub side_effects: Vec, + pub state_transitions: Vec, + pub constraints: Vec, + pub locks: Vec, + pub struct_specs: Vec, +} + +/// Trait for extracting API specifications from different sources +pub trait ApiExtractor { + /// Extract all API specifications from the source + fn extract_all(&self) -> Result>; + + /// Extract a specific API specification by name + fn extract_by_name(&self, name: &str) -> Result>; + + /// 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) =3D &spec.description { + formatter.description(writer, desc)?; + } + + if let Some(long_desc) =3D &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) =3D &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) =3D &spec.notes { + formatter.notes(writer, notes)?; + } + + if let Some(examples) =3D &spec.examples { + formatter.examples(writer, examples)?; + } + + // Display sysfs-specific fields + if spec.api_type =3D=3D "sysfs" { + if let Some(subsystem) =3D &spec.subsystem { + formatter.sysfs_subsystem(writer, subsystem)?; + } + if let Some(path) =3D &spec.sysfs_path { + formatter.sysfs_path(writer, path)?; + } + if let Some(perms) =3D &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().unwr= ap_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_o= r(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/ext= ractor/source_parser.rs new file mode 100644 index 0000000000000..4138c128b7a2e --- /dev/null +++ b/tools/kapi/src/extractor/source_parser.rs @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +use super::kerneldoc_parser::KerneldocParserImpl; +use super::{display_api_spec, ApiExtractor, ApiSpec}; +use crate::formatter::OutputFormatter; +use anyhow::{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: KerneldocParserImpl, + syscall_regex: Regex, + ioctl_regex: Regex, + function_regex: Regex, +} + +impl SourceExtractor { + pub fn new(path: &str) -> Result { + Ok(SourceExtractor { + path: path.to_string(), + parser: KerneldocParserImpl::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> { + let content =3D fs::read_to_string(path) + .with_context(|| format!("Failed to read file: {}", path.displ= ay()))?; + + self.extract_from_content(&content) + } + + fn extract_from_content(&self, content: &str) -> Result> { + let mut specs =3D Vec::new(); + let mut in_kerneldoc =3D false; + let mut current_doc =3D String::new(); + let lines: Vec<&str> =3D content.lines().collect(); + let mut i =3D 0; + + while i < lines.len() { + let line =3D lines[i]; + + // Start of kerneldoc comment + if line.trim_start().starts_with("/**") { + in_kerneldoc =3D true; + current_doc.clear(); + i +=3D 1; + continue; + } + + // Inside kerneldoc comment + if in_kerneldoc { + if line.contains("*/") { + in_kerneldoc =3D false; + + // Check if this kerneldoc has KAPI annotations + if current_doc.contains("context-flags:") + || current_doc.contains("param-count:") + || current_doc.contains("side-effect:") + || current_doc.contains("state-trans:") + || current_doc.contains("error-code:") + { + // Look ahead for the function declaration + if let Some((name, api_type, signature)) =3D + self.find_function_after(&lines, i + 1) + { + if let Ok(spec) =3D self.parser.parse_kerneldo= c( + ¤t_doc, + &name, + &api_type, + Some(&signature), + ) { + specs.push(spec); + } + } + } + } else { + // Remove leading asterisk and preserve content + let cleaned =3D if let Some(stripped) =3D line.trim_st= art().strip_prefix("*") { + if let Some(no_space) =3D stripped.strip_prefix(' = ') { + no_space + } else { + stripped + } + } else { + line.trim_start() + }; + current_doc.push_str(cleaned); + current_doc.push('\n'); + } + } + + i +=3D 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 =3D lines[i]; + + // Skip empty lines + if line.trim().is_empty() { + continue; + } + + // Check for SYSCALL_DEFINE + if let Some(caps) =3D self.syscall_regex.captures(line) { + let name =3D format!("sys_{}", caps.get(1).unwrap().as_str= ()); + let signature =3D self.extract_syscall_signature(lines, i); + return Some((name, "syscall".to_string(), signature)); + } + + // Check for ioctl function + if let Some(caps) =3D self.ioctl_regex.captures(line) { + let name =3D 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) =3D self.function_regex.captures(line) { + let name =3D 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 fun= ction declaration + if !line.starts_with(' ') && !line.starts_with('\t') && !line.= trim().is_empty() { + break; + } + } + + None + } + + fn extract_syscall_signature(&self, lines: &[&str], start: usize) -> S= tring { + // Extract the full SYSCALL_DEFINE signature + let mut sig =3D String::new(); + let mut in_paren =3D false; + let mut paren_count =3D 0; + + for line in lines.iter().skip(start).take(20) { + let line =3D *line; + + // Start of SYSCALL_DEFINE + if line.contains("SYSCALL_DEFINE") { + if let Some(pos) =3D line.find('(') { + sig.push_str(&line[pos..]); + in_paren =3D true; + paren_count =3D line[pos..].chars().filter(|&c| c =3D= =3D '(').count() + - line[pos..].chars().filter(|&c| c =3D=3D ')').co= unt(); + } + } else if in_paren { + sig.push(' '); + sig.push_str(line.trim()); + paren_count +=3D line.chars().filter(|&c| c =3D=3D '(').co= unt(); + paren_count =3D + paren_count.saturating_sub(line.chars().filter(|&c| c = =3D=3D ')').count()); + + if paren_count =3D=3D 0 { + break; + } + } + } + + sig + } +} + +impl ApiExtractor for SourceExtractor { + fn extract_all(&self) -> Result> { + let path =3D Path::new(&self.path); + let mut all_specs =3D 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 =3D=3D "c" || ext =3D=3D "h= ") + }) + { + match self.extract_from_file(entry.path()) { + Ok(specs) =3D> all_specs.extend(specs), + Err(e) =3D> { + eprintln!("Warning: failed to parse {}: {}", entry= .path().display(), e); + } + } + } + } + + Ok(all_specs) + } + + fn extract_by_name(&self, name: &str) -> Result> { + let all_specs =3D self.extract_all()?; + Ok(all_specs.into_iter().find(|s| s.name =3D=3D name)) + } + + fn display_api_details( + &self, + api_name: &str, + formatter: &mut dyn OutputFormatter, + output: &mut dyn Write, + ) -> Result<()> { + if let Some(spec) =3D 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_syscall_define3() { + let content =3D r#" +/** + * sys_open - open a file + * context-flags: KAPI_CTX_PROCESS + * param-count: 3 + * @filename: pathname to open + * param-type: filename, KAPI_TYPE_STRING + * error-code: ENOENT + */ +SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, = mode) +{ + return 0; +} +"#; + let ext =3D make_extractor(); + let specs =3D 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 =3D r#" +/** + * sys_close - close a file descriptor + * context-flags: KAPI_CTX_PROCESS + * @fd: file descriptor to close + * error-code: EBADF + */ +SYSCALL_DEFINE1(close, unsigned int, fd) +{ + return 0; +} +"#; + let ext =3D make_extractor(); + let specs =3D 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 =3D r#" +/** + * sys_mmap - map memory + * context-flags: KAPI_CTX_PROCESS + * error-code: ENOMEM + */ +SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len, unsigned lo= ng, prot, + unsigned long, flags, unsigned long, fd, unsigned long, offset) +{ + return 0; +} +"#; + let ext =3D make_extractor(); + let specs =3D 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 =3D r#" +/** + * my_ioctl - handle ioctl + * context-flags: KAPI_CTX_PROCESS + * error-code: EINVAL + */ +static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long ar= g) +{ + return 0; +} +"#; + let ext =3D make_extractor(); + let specs =3D 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 =3D vec!["", "", "SYSCALL_DEFINE2(foo, int, bar, int, ba= z)", "{"]; + let ext =3D make_extractor(); + let result =3D ext.find_function_after(&lines, 0); + assert!(result.is_some()); + let (name, api_type, _sig) =3D result.unwrap(); + assert_eq!(name, "sys_foo"); + assert_eq!(api_type, "syscall"); + } + + #[test] + fn find_function_after_returns_none_for_no_match() { + // No function declaration within lookahead range + let lines =3D vec!["#include ", "#define FOO 1", "/* c= omment */"]; + let ext =3D make_extractor(); + let result =3D ext.find_function_after(&lines, 0); + // The function_regex may or may not match #define, but let's check + // that a pure preprocessor/comment block doesn't false-positive o= n syscall/ioctl + if let Some((_, api_type, _)) =3D &result { + assert_ne!(api_type, "syscall"); + assert_ne!(api_type, "ioctl"); + } + } + + #[test] + fn find_function_after_detects_regular_function() { + let lines =3D vec!["", "int do_something(struct task_struct *task)= ", "{"]; + let ext =3D make_extractor(); + let result =3D ext.find_function_after(&lines, 0); + assert!(result.is_some()); + let (name, api_type, _) =3D 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 =3D r#" +/** + * my_func - does stuff + * @arg: an argument + */ +void my_func(int arg) +{ +} +"#; + let ext =3D make_extractor(); + let specs =3D ext.extract_from_content(content).unwrap(); + assert!(specs.is_empty()); + } + + #[test] + fn multiple_syscalls_in_one_file() { + let content =3D r#" +/** + * sys_read - read from fd + * context-flags: KAPI_CTX_PROCESS + * error-code: EBADF + */ +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-code: EBADF + */ +SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t,= count) +{ + return 0; +} +"#; + let ext =3D make_extractor(); + let specs =3D 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..b03a75b289c1f --- /dev/null +++ b/tools/kapi/src/extractor/vmlinux/binary_utils.rs @@ -0,0 +1,462 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +// 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 =3D 16; + pub const MAX_ERRORS: usize =3D 32; + pub const MAX_CONSTRAINTS: usize =3D 32; + pub const MAX_LOCKS: usize =3D 16; + pub const MAX_CAPABILITIES: usize =3D 8; + pub const MAX_SIGNALS: usize =3D 32; + pub const MAX_STRUCT_SPECS: usize =3D 8; + pub const MAX_SIDE_EFFECTS: usize =3D 32; + pub const MAX_STATE_TRANS: usize =3D 8; + + pub const NAME: usize =3D 0; + pub const DESC: usize =3D 0; +} + +/// 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: u6= 4) -> Option { + if vaddr =3D=3D 0 { + return None; + } + for sh in &elf.section_headers { + let start =3D sh.sh_addr; + let end =3D start.checked_add(sh.sh_size)?; + if vaddr < start || vaddr >=3D end { + continue; + } + // File-backed sections only (skip SHT_NOBITS etc.) + if sh.sh_type =3D=3D goblin::elf::section_header::SHT_NOBITS { + return None; + } + let rel =3D (vaddr - start) as usize; + let file_start =3D sh.sh_offset as usize + rel; + if file_start >=3D data.len() { + return None; + } + let tail =3D &data[file_start..]; + let nul =3D tail.iter().position(|&b| b =3D=3D 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>, +} + +impl<'a> DataReader<'a> { + pub fn new(data: &'a [u8], offset: usize, endian: Endian, is_64bit: bo= ol) -> Self { + Self { + data, + pos: offset, + endian, + is_64bit, + resolver: None, + } + } + + pub fn with_resolver(mut self, resolver: StringResolver<'a>) -> Self { + self.resolver =3D 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 =3D self.pos % align; + if rem !=3D 0 { + self.pos =3D (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 { + 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 <=3D self.data.len() { + let bytes =3D &self.data[self.pos..self.pos + len]; + self.pos +=3D len; + Some(bytes) + } else { + None + } + } + + pub fn read_u32(&mut self) -> Option { + self.align_to(4); + let b: [u8; 4] =3D self.read_bytes(4)?.try_into().unwrap(); + Some(match self.endian { + Endian::Little =3D> u32::from_le_bytes(b), + Endian::Big =3D> u32::from_be_bytes(b), + }) + } + + pub fn read_u8(&mut self) -> Option { + self.read_bytes(1).map(|b| b[0]) + } + + pub fn read_i32(&mut self) -> Option { + self.align_to(4); + let b: [u8; 4] =3D self.read_bytes(4)?.try_into().unwrap(); + Some(match self.endian { + Endian::Little =3D> i32::from_le_bytes(b), + Endian::Big =3D> i32::from_be_bytes(b), + }) + } + + pub fn read_u64(&mut self) -> Option { + self.align_to(8); + let b: [u8; 8] =3D self.read_bytes(8)?.try_into().unwrap(); + Some(match self.endian { + Endian::Little =3D> u64::from_le_bytes(b), + Endian::Big =3D> u64::from_be_bytes(b), + }) + } + + pub fn read_i64(&mut self) -> Option { + self.align_to(8); + let b: [u8; 8] =3D self.read_bytes(8)?.try_into().unwrap(); + Some(match self.endian { + Endian::Little =3D> i64::from_le_bytes(b), + Endian::Big =3D> i64::from_be_bytes(b), + }) + } + + /// Read a target-sized unsigned value (4 bytes for 32-bit, 8 bytes fo= r 64-bit). + pub fn read_usize(&mut self) -> Option { + 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] =3D self.read_bytes(8)?.try_into().unwrap(); + Some(match self.endian { + Endian::Little =3D> u64::from_le_bytes(b) as usize, + Endian::Big =3D> u64::from_be_bytes(b) as usize, + }) + } else { + let b: [u8; 4] =3D self.read_bytes(4)?.try_into().unwrap(); + Some(match self.endian { + Endian::Little =3D> u32::from_le_bytes(b) as usize, + Endian::Big =3D> u32::from_be_bytes(b) as usize, + }) + } + } + + pub fn skip(&mut self, len: usize) { + self.pos =3D (self.pos + len).min(self.data.len()); + } + + // Helper methods for common patterns + pub fn read_bool(&mut self) -> Option { + self.read_u8().map(|v| v !=3D 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 { + let vaddr =3D self.read_ptr()?; + let resolver =3D 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() + } +} + +// Structure layout definitions for calculating sizes +pub fn signal_mask_spec_layout_size() -> usize { + // Packed structure from struct kapi_signal_mask_spec + sizes::NAME + // mask_name + 4 * sizes::MAX_SIGNALS + // signals array + 4 + // signal_count + sizes::DESC // description +} + +pub fn struct_field_layout_size() -> usize { + // Packed structure from struct kapi_struct_field + sizes::NAME + // name + 4 + // type (enum) + sizes::NAME + // type_name + 8 + // offset (size_t) + 8 + // size (size_t) + 4 + // flags + 4 + // constraint_type (enum) + 8 + // min_value (s64) + 8 + // max_value (s64) + 8 + // valid_mask (u64) + sizes::DESC + // enum_values + sizes::DESC // description +} + +#[cfg(test)] +mod tests { + use super::*; + + // ---- DataReader little-endian tests ---- + + #[test] + fn read_u32_little_endian() { + let data =3D [0x78, 0x56, 0x34, 0x12]; + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_u32(), Some(0x12345678)); + } + + #[test] + fn read_u32_big_endian() { + let data =3D [0x12, 0x34, 0x56, 0x78]; + let mut reader =3D DataReader::new(&data, 0, Endian::Big, true); + assert_eq!(reader.read_u32(), Some(0x12345678)); + } + + #[test] + fn read_u64_little_endian() { + let data =3D 0xDEADBEEFCAFEBABEu64.to_le_bytes(); + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_u64(), Some(0xDEADBEEFCAFEBABE)); + } + + #[test] + fn read_u64_big_endian() { + let data =3D 0xDEADBEEFCAFEBABEu64.to_be_bytes(); + let mut reader =3D DataReader::new(&data, 0, Endian::Big, true); + assert_eq!(reader.read_u64(), Some(0xDEADBEEFCAFEBABE)); + } + + #[test] + fn read_i32_little_endian_negative() { + let val: i32 =3D -42; + let data =3D val.to_le_bytes(); + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_i32(), Some(-42)); + } + + #[test] + fn read_i32_big_endian_negative() { + let val: i32 =3D -1; + let data =3D val.to_be_bytes(); + let mut reader =3D DataReader::new(&data, 0, Endian::Big, true); + assert_eq!(reader.read_i32(), Some(-1)); + } + + #[test] + fn read_i64_little_endian() { + let val: i64 =3D -9999999999; + let data =3D val.to_le_bytes(); + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_i64(), Some(-9999999999)); + } + + #[test] + fn read_i64_big_endian() { + let val: i64 =3D i64::MIN; + let data =3D val.to_be_bytes(); + let mut reader =3D 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 =3D 0x00000000FFFFFFFF; + let data =3D val.to_le_bytes(); + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_usize(), Some(0xFFFFFFFF)); + } + + #[test] + fn read_usize_32bit() { + let val: u32 =3D 0xABCD1234; + let data =3D val.to_le_bytes(); + let mut reader =3D 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 =3D [0u8; 8]; + data[..4].copy_from_slice(&42u32.to_le_bytes()); + data[4..8].copy_from_slice(&99u32.to_le_bytes()); + let mut reader =3D 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 =3D [0x01, 0x02, 0x03]; // only 3 bytes, need 4 + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_u32(), None); + } + + #[test] + fn read_u64_past_end_returns_none() { + let data =3D [0u8; 7]; // only 7 bytes, need 8 + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_u64(), None); + } + + #[test] + fn read_bytes_past_end_returns_none() { + let data =3D [0u8; 4]; + let mut reader =3D 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 =3D [0xFF, 0xFF, 0xFF, 0xFF, 0x78, 0x56, 0x34, 0x12]; + let mut reader =3D 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 =3D [0xDE, 0xAD, 0xBE, 0xEF, 0x78, 0x56, 0x34, 0x12]; + let mut reader =3D 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 =3D [0x78, 0x56, 0x34, 0x12]; + let mut reader =3D 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 =3D [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00]; + let mut reader =3D 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 =3D [0, 1, 255]; + let mut reader =3D 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 skip_advances_position() { + let data =3D [0u8; 20]; + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + reader.skip(10); + assert_eq!(reader.pos, 10); + reader.skip(5); + assert_eq!(reader.pos, 15); + } + + #[test] + fn skip_clamps_to_data_len() { + let data =3D [0u8; 10]; + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + reader.skip(100); + assert_eq!(reader.pos, 10); + } + + #[test] + fn sequential_reads_advance_position() { + let mut data =3D [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 =3D 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 =3D [0u8; 10]; + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + // read_cstring returns None when null_pos =3D=3D 0 + // read_optional_string filters empty strings, but read_cstring wo= n't return empty + assert_eq!(reader.read_optional_string(10), None); + } + + #[test] + fn read_string_or_default_with_empty() { + let data =3D [0u8; 10]; + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_string_or_default(10), ""); + } + + #[test] + fn read_u8_value() { + let data =3D [0x42]; + let mut reader =3D DataReader::new(&data, 0, Endian::Little, true); + assert_eq!(reader.read_u8(), Some(0x42)); + } +} diff --git a/tools/kapi/src/extractor/vmlinux/magic_finder.rs b/tools/kapi/= src/extractor/vmlinux/magic_finder.rs new file mode 100644 index 0000000000000..65081852ffaaf --- /dev/null +++ b/tools/kapi/src/extractor/vmlinux/magic_finder.rs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +use super::binary_utils::Endian; + +// Magic markers for each section +pub const MAGIC_PARAM: u32 =3D 0x4B415031; // 'KAP1' +pub const MAGIC_RETURN: u32 =3D 0x4B415232; // 'KAR2' +pub const MAGIC_ERROR: u32 =3D 0x4B414533; // 'KAE3' +pub const MAGIC_LOCK: u32 =3D 0x4B414C34; // 'KAL4' +pub const MAGIC_CONSTRAINT: u32 =3D 0x4B414335; // 'KAC5' +pub const MAGIC_INFO: u32 =3D 0x4B414936; // 'KAI6' +pub const MAGIC_SIGNAL: u32 =3D 0x4B415337; // 'KAS7' +pub const MAGIC_SIGMASK: u32 =3D 0x4B414D38; // 'KAM8' +pub const MAGIC_STRUCT: u32 =3D 0x4B415439; // 'KAT9' +pub const MAGIC_EFFECT: u32 =3D 0x4B414641; // 'KAFA' +pub const MAGIC_TRANS: u32 =3D 0x4B415442; // 'KATB' +pub const MAGIC_CAP: u32 =3D 0x4B414343; // 'KACC' + +fn read_u32_endian(bytes: &[u8], endian: Endian) -> u32 { + let b =3D [bytes[0], bytes[1], bytes[2], bytes[3]]; + match endian { + Endian::Little =3D> u32::from_le_bytes(b), + Endian::Big =3D> u32::from_be_bytes(b), + } +} + +pub struct MagicOffsets { + pub param_offset: Option, + pub return_offset: Option, + pub error_offset: Option, + pub lock_offset: Option, + pub constraint_offset: Option, + pub info_offset: Option, + pub signal_offset: Option, + pub sigmask_offset: Option, + pub struct_offset: Option, + pub effect_offset: Option, + pub trans_offset: Option, + pub cap_offset: Option, +} + +impl MagicOffsets { + /// Find magic markers in the provided data slice + /// data: slice of data to search (typically one spec's worth) + /// base_offset: absolute offset where this slice starts in the full b= uffer + pub fn find_in_data(data: &[u8], base_offset: usize, endian: Endian) -= > Self { + let mut offsets =3D MagicOffsets { + param_offset: None, + return_offset: None, + error_offset: None, + lock_offset: None, + constraint_offset: None, + info_offset: None, + signal_offset: None, + sigmask_offset: None, + struct_offset: None, + effect_offset: None, + trans_offset: None, + cap_offset: None, + }; + + // Scan through data looking for magic markers + // Only find the first occurrence of each magic to avoid cross-spe= c contamination + let mut i =3D 0; + while i + 4 <=3D data.len() { + let bytes =3D &data[i..i + 4]; + let value =3D read_u32_endian(bytes, endian); + + match value { + MAGIC_PARAM if offsets.param_offset.is_none() =3D> { + offsets.param_offset =3D Some(base_offset + i); + } + MAGIC_RETURN if offsets.return_offset.is_none() =3D> { + offsets.return_offset =3D Some(base_offset + i); + } + MAGIC_ERROR if offsets.error_offset.is_none() =3D> { + offsets.error_offset =3D Some(base_offset + i); + } + MAGIC_LOCK if offsets.lock_offset.is_none() =3D> { + offsets.lock_offset =3D Some(base_offset + i); + } + MAGIC_CONSTRAINT if offsets.constraint_offset.is_none() = =3D> { + offsets.constraint_offset =3D Some(base_offset + i); + } + MAGIC_INFO if offsets.info_offset.is_none() =3D> { + offsets.info_offset =3D Some(base_offset + i); + } + MAGIC_SIGNAL if offsets.signal_offset.is_none() =3D> { + offsets.signal_offset =3D Some(base_offset + i); + } + MAGIC_SIGMASK if offsets.sigmask_offset.is_none() =3D> { + offsets.sigmask_offset =3D Some(base_offset + i); + } + MAGIC_STRUCT if offsets.struct_offset.is_none() =3D> { + offsets.struct_offset =3D Some(base_offset + i); + } + MAGIC_EFFECT if offsets.effect_offset.is_none() =3D> { + offsets.effect_offset =3D Some(base_offset + i); + } + MAGIC_TRANS if offsets.trans_offset.is_none() =3D> { + offsets.trans_offset =3D Some(base_offset + i); + } + MAGIC_CAP if offsets.cap_offset.is_none() =3D> { + offsets.cap_offset =3D Some(base_offset + i); + } + _ =3D> {} + } + + i +=3D 1; + } + + offsets + } +} diff --git a/tools/kapi/src/extractor/vmlinux/mod.rs b/tools/kapi/src/extra= ctor/vmlinux/mod.rs new file mode 100644 index 0000000000000..41c9bf1b06591 --- /dev/null +++ b/tools/kapi/src/extractor/vmlinux/mod.rs @@ -0,0 +1,857 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +use super::{ + ApiExtractor, ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, Lock= Spec, ParamSpec, + ReturnSpec, SideEffectSpec, SignalMaskSpec, SignalSpec, StateTransitio= nSpec, StructFieldSpec, + StructSpec, +}; +use crate::formatter::OutputFormatter; +use anyhow::{Context, Result}; +use goblin::elf::Elf; +use std::fs; +use std::io::Write; + +mod binary_utils; +mod magic_finder; +use binary_utils::{ + signal_mask_spec_layout_size, sizes, struct_field_layout_size, DataRea= der, Endian, +}; + +// Helper to convert empty strings to None +fn opt_string(s: String) -> Option { + if s.is_empty() { + None + } else { + Some(s) + } +} + +pub struct VmlinuxExtractor { + vmlinux: Vec, + specs: Vec, + 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 { + let vmlinux =3D fs::read(vmlinux_path) + .with_context(|| format!("Failed to read vmlinux file: {vmlinu= x_path}"))?; + + let elf =3D Elf::parse(&vmlinux).context("Failed to parse ELF file= ")?; + let endian =3D if elf.little_endian { + Endian::Little + } else { + Endian::Big + }; + let is_64bit =3D elf.is_64; + + // Locate the .kapi_specs section boundaries. + let mut start_addr =3D None; + let mut stop_addr =3D None; + for sym in &elf.syms { + if let Some(name) =3D elf.strtab.get_at(sym.st_name) { + match name { + "__start_kapi_specs" =3D> start_addr =3D Some(sym.st_v= alue), + "__stop_kapi_specs" =3D> stop_addr =3D Some(sym.st_val= ue), + _ =3D> {} + } + } + } + let start =3D start_addr.context("Could not find __start_kapi_spec= s symbol")?; + let stop =3D stop_addr.context("Could not find __stop_kapi_specs s= ymbol")?; + if stop <=3D start { + anyhow::bail!("No kernel API specifications found in vmlinux"); + } + + // `.kapi_specs` is a tightly-packed array of `struct kernel_api_s= pec *` + // pointers; walk them to find each real spec's vaddr, then resolv= e to + // a file offset inside `vmlinux`. Pointer width tracks the target + // (4 bytes for 32-bit, 8 bytes for 64-bit). + let ptr_size =3D if is_64bit { 8usize } else { 4 }; + let ptr_count =3D ((stop - start) as usize) / ptr_size; + let ptr_file_off =3D + vaddr_to_file_offset(&elf, start).context("Could not locate .k= api_specs in file")?; + + let read_ptr =3D |raw: &[u8]| -> u64 { + match (endian, is_64bit) { + (Endian::Little, true) =3D> u64::from_le_bytes(raw.try_int= o().unwrap()), + (Endian::Big, true) =3D> u64::from_be_bytes(raw.try_into()= .unwrap()), + (Endian::Little, false) =3D> u32::from_le_bytes(raw.try_in= to().unwrap()) as u64, + (Endian::Big, false) =3D> u32::from_be_bytes(raw.try_into(= ).unwrap()) as u64, + } + }; + + let mut specs =3D Vec::with_capacity(ptr_count); + for i in 0..ptr_count { + let p =3D ptr_file_off + i * ptr_size; + if p + ptr_size > vmlinux.len() { + break; + } + let spec_vaddr =3D read_ptr(&vmlinux[p..p + ptr_size]); + if spec_vaddr =3D=3D 0 { + continue; + } + let Some(spec_file_off) =3D vaddr_to_file_offset(&elf, spec_va= ddr) 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 =3D read_ptr(&vmlinux[spec_file_off..spec_file_= off + ptr_size]); + let name =3D + binary_utils::resolve_vaddr_string(&elf, &vmlinux, name_va= ddr).unwrap_or_default(); + if name.is_empty() { + continue; + } + let api_type =3D 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 { + for sh in &elf.section_headers { + let start =3D sh.sh_addr; + let end =3D start.checked_add(sh.sh_size)?; + if vaddr >=3D start && vaddr < end { + if sh.sh_type =3D=3D 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 { + parse_binary_to_api_spec(&self.vmlinux, file_offset, self.endian, = self.is_64bit) + } +} + +impl ApiExtractor for VmlinuxExtractor { + fn extract_all(&self) -> Result> { + Ok(self + .specs + .iter() + .map(|spec| { + self.parse_at(spec.file_offset).unwrap_or_else(|_| ApiSpec= { + name: spec.name.clone(), + api_type: spec.api_type.clone(), + ..Default::default() + }) + }) + .collect()) + } + + fn extract_by_name(&self, api_name: &str) -> Result> { + if let Some(spec) =3D self.specs.iter().find(|s| s.name =3D=3D 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) =3D self.specs.iter().find(|s| s.name =3D=3D api= _name) { + let api_spec =3D self.parse_at(spec.file_offset)?; + super::display_api_spec(&api_spec, formatter, writer)?; + } + Ok(()) + } +} + +/// Helper to read count and parse array items with optional magic offset +fn parse_array_with_magic( + reader: &mut DataReader, + magic_offset: Option, + max_items: u32, + parse_fn: F, +) -> Vec +where + F: Fn(&mut DataReader, usize) -> Option, +{ + // Read count - position at magic+4 if magic offset exists + let count =3D if let Some(offset) =3D magic_offset { + reader.pos =3D offset + 4; + reader.read_u32() + } else { + reader.read_u32() + }; + + let mut items =3D Vec::new(); + if let Some(count) =3D count { + // Position at start of array data if magic offset exists + if let Some(offset) =3D magic_offset { + reader.pos =3D offset + 8; // +4 for magic, +4 for count + } + // Parse items up to max_items. Each element-parse is followed + // by align_to(ptr_size) so the next element starts on the + // struct's natural alignment boundary (kernel structs are no + // longer __packed, so the compiler may insert trailing + // padding after bools / u8 fields). + let align =3D reader.ptr_size(); + for i in 0..count.min(max_items) as usize { + if let Some(item) =3D parse_fn(reader, i) { + items.push(item); + } + reader.align_to(align); + } + } + items +} + +fn parse_binary_to_api_spec( + data: &[u8], + offset: usize, + endian: Endian, + is_64bit: bool, +) -> Result { + let elf =3D Elf::parse(data).context("Failed to re-parse ELF for strin= g resolution")?; + let resolver =3D binary_utils::StringResolver { + elf: &elf, + vmlinux: data, + }; + let mut reader =3D DataReader::new(data, offset, endian, is_64bit).wit= h_resolver(resolver); + + // Bound magic-marker search to roughly sizeof(struct kernel_api_spec). + // The packed-const-char-pointer layout is ~25 KB per spec; 32 KB gives + // headroom without letting the finder leak into the next spec (or + // unrelated rodata) and pick up stray magic markers. + let search_end =3D (offset + 0x8000).min(data.len()); + let spec_data =3D &data[offset..search_end]; + let magic_offsets =3D magic_finder::MagicOffsets::find_in_data(spec_da= ta, offset, endian); + + // 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 =3D reader + .read_optional_string(sizes::NAME) + .ok_or_else(|| anyhow::anyhow!("Failed to read API name"))?; + + // Determine API type + let api_type =3D if name.starts_with("sys_") { + "syscall" + } else if name.ends_with("_ioctl") { + "ioctl" + } else if name.contains("sysfs") { + "sysfs" + } else { + "function" + } + .to_string(); + + // Read version (u32) + let version =3D reader.read_u32().map(|v| v.to_string()); + + // Read description (512 bytes) + let description =3D reader + .read_optional_string(sizes::DESC) + .filter(|s| !s.is_empty()); + + // Read long_description (2048 bytes) + let long_description =3D reader + .read_optional_string(sizes::DESC) + .filter(|s| !s.is_empty()); + + // Read context_flags (u32) + let context_flags =3D parse_context_flags(&mut reader); + + // Parse params array + let parameters =3D parse_array_with_magic( + &mut reader, + magic_offsets.param_offset, + sizes::MAX_PARAMS as u32, + parse_param, + ); + + // Read return_spec - position using magic offset if available + if let Some(offset) =3D magic_offsets.return_offset { + reader.pos =3D offset + 4; // skip past the return_magic u32 + } + let return_spec =3D parse_return_spec(&mut reader); + + // Parse errors array + let errors =3D parse_array_with_magic( + &mut reader, + magic_offsets.error_offset, + sizes::MAX_ERRORS as u32, + |r, _| parse_error(r), + ); + + // Parse locks array + let locks =3D parse_array_with_magic( + &mut reader, + magic_offsets.lock_offset, + sizes::MAX_LOCKS as u32, + |r, _| parse_lock(r), + ); + + // Parse constraints array + let constraints =3D parse_array_with_magic( + &mut reader, + magic_offsets.constraint_offset, + sizes::MAX_CONSTRAINTS as u32, + |r, _| parse_constraint(r), + ); + + // Read examples and notes - position reader at info section if magic = found + let (examples, notes) =3D if let Some(info_offset) =3D magic_offsets.i= nfo_offset { + reader.pos =3D info_offset + 4; // +4 to skip magic + let examples =3D reader + .read_optional_string(sizes::DESC) + .filter(|s| !s.is_empty()); + let notes =3D reader + .read_optional_string(sizes::DESC) + .filter(|s| !s.is_empty()); + (examples, notes) + } else { + let examples =3D reader + .read_optional_string(sizes::DESC) + .filter(|s| !s.is_empty()); + let notes =3D reader + .read_optional_string(sizes::DESC) + .filter(|s| !s.is_empty()); + (examples, notes) + }; + + // Parse signals array + let signals =3D parse_array_with_magic( + &mut reader, + magic_offsets.signal_offset, + sizes::MAX_SIGNALS as u32, + |r, _| parse_signal(r), + ); + + // Read signal_mask_count (u32) + let signal_mask_count =3D reader.read_u32(); + + // Parse signal_masks array + let mut signal_masks =3D Vec::new(); + if let Some(count) =3D signal_mask_count { + for i in 0..sizes::MAX_SIGNALS { + if i < count as usize { + if let Some(mask) =3D parse_signal_mask(&mut reader) { + signal_masks.push(mask); + } + } else { + reader.skip(signal_mask_spec_layout_size()); + } + } + } else { + reader.skip(signal_mask_spec_layout_size() * sizes::MAX_SIGNALS); + } + + // Parse struct_specs array + let struct_specs =3D parse_array_with_magic( + &mut reader, + magic_offsets.struct_offset, + sizes::MAX_STRUCT_SPECS as u32, + |r, _| parse_struct_spec(r), + ); + + // According to the C struct, the order is: + // side_effect_count, side_effects array, state_trans_count, state_tra= nsitions array, + // capability_count, capabilities array + + // Parse side_effects array + let side_effects =3D parse_array_with_magic( + &mut reader, + magic_offsets.effect_offset, + sizes::MAX_SIDE_EFFECTS as u32, + |r, _| parse_side_effect(r), + ); + + // Parse state_transitions array + let state_transitions =3D parse_array_with_magic( + &mut reader, + magic_offsets.trans_offset, + sizes::MAX_STATE_TRANS as u32, + |r, _| parse_state_transition(r), + ); + + // Parse capabilities array + let capabilities =3D parse_array_with_magic( + &mut reader, + magic_offsets.cap_offset, + sizes::MAX_CAPABILITIES as u32, + |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 { + const KAPI_CTX_PROCESS: u32 =3D 1 << 0; + const KAPI_CTX_SOFTIRQ: u32 =3D 1 << 1; + const KAPI_CTX_HARDIRQ: u32 =3D 1 << 2; + const KAPI_CTX_NMI: u32 =3D 1 << 3; + const KAPI_CTX_ATOMIC: u32 =3D 1 << 4; + const KAPI_CTX_SLEEPABLE: u32 =3D 1 << 5; + const KAPI_CTX_PREEMPT_DISABLED: u32 =3D 1 << 6; + const KAPI_CTX_IRQ_DISABLED: u32 =3D 1 << 7; + + if let Some(flags) =3D reader.read_u32() { + let mut parts =3D Vec::new(); + + if flags & KAPI_CTX_PROCESS !=3D 0 { + parts.push("KAPI_CTX_PROCESS"); + } + if flags & KAPI_CTX_SOFTIRQ !=3D 0 { + parts.push("KAPI_CTX_SOFTIRQ"); + } + if flags & KAPI_CTX_HARDIRQ !=3D 0 { + parts.push("KAPI_CTX_HARDIRQ"); + } + if flags & KAPI_CTX_NMI !=3D 0 { + parts.push("KAPI_CTX_NMI"); + } + if flags & KAPI_CTX_ATOMIC !=3D 0 { + parts.push("KAPI_CTX_ATOMIC"); + } + if flags & KAPI_CTX_SLEEPABLE !=3D 0 { + parts.push("KAPI_CTX_SLEEPABLE"); + } + if flags & KAPI_CTX_PREEMPT_DISABLED !=3D 0 { + parts.push("KAPI_CTX_PREEMPT_DISABLED"); + } + if flags & KAPI_CTX_IRQ_DISABLED !=3D 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= { + let name =3D reader.read_optional_string(sizes::NAME)?; + let type_name =3D reader.read_optional_string(sizes::NAME)?; + let param_type =3D reader.read_u32()?; + let flags =3D reader.read_u32()?; + let size =3D reader.read_usize()?; + let alignment =3D reader.read_usize()?; + let min_value =3D reader.read_i64()?; + let max_value =3D reader.read_i64()?; + let valid_mask =3D reader.read_u64()?; + + // Skip enum_values pointer (8 bytes) + reader.skip(8); + let _enum_count =3D reader.read_u32()?; // Must use ? to propagate err= ors + let constraint_type =3D reader.read_u32()?; + // Skip validate function pointer (8 bytes) + reader.skip(8); + + let description =3D reader.read_string_or_default(sizes::DESC); + let constraint =3D reader.read_optional_string(sizes::DESC); + let size_param_idx_raw =3D reader.read_i32()?; // Must use ? to propag= ate errors + let _size_multiplier =3D reader.read_usize()?; // Must use ? to propag= ate 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`. + let size_param_idx =3D if size_param_idx_raw > 0 { + Some((size_param_idx_raw - 1) as u32) + } else { + None + }; + + Some(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: vec![], + size: Some(size as u32), + alignment: Some(alignment as u32), + size_param_idx, + }) +} + +fn parse_return_spec(reader: &mut DataReader) -> Option { + // Read type_name, but treat empty as valid (will be empty string) + let type_name =3D reader.read_string_or_default(sizes::NAME); + + // Read return_type and check_type + let return_type =3D reader.read_u32().unwrap_or(0); + let check_type =3D reader.read_u32().unwrap_or(0); + let success_value =3D reader.read_i64().unwrap_or(0); + let success_min =3D reader.read_i64().unwrap_or(0); + let success_max =3D reader.read_i64().unwrap_or(0); + + // Skip error_values pointer (8 bytes) + reader.skip(8); + let _error_count =3D reader.read_u32().unwrap_or(0); // Don't fail on = return spec + // Skip is_success = function pointer (8 bytes) + reader.skip(8); + + let description =3D reader.read_string_or_default(sizes::DESC); + + // Return a spec even if type_name is empty, as long as we have some d= ata + // The type_name might be a string like "KAPI_TYPE_INT" that gets stor= ed literally + if type_name.is_empty() && return_type =3D=3D 0 && check_type =3D=3D 0= && success_value =3D=3D 0 { + // No return spec at all + return None; + } + + Some(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: vec![], + }) +} + +fn parse_error(reader: &mut DataReader) -> Option { + let error_code =3D reader.read_i32()?; + let name =3D reader.read_optional_string(sizes::NAME)?; + let condition =3D reader.read_string_or_default(sizes::DESC); + let description =3D reader.read_string_or_default(sizes::DESC); + + Some(ErrorSpec { + error_code, + name, + condition, + description, + }) +} + +fn parse_lock(reader: &mut DataReader) -> Option { + let lock_name =3D reader.read_optional_string(sizes::NAME)?; + let lock_type =3D reader.read_u32()?; + let scope =3D reader.read_u32()?; + let description =3D reader.read_string_or_default(sizes::DESC); + + Some(LockSpec { + lock_name, + lock_type, + scope, + description, + }) +} + +fn parse_constraint(reader: &mut DataReader) -> Option { + let name =3D reader.read_optional_string(sizes::NAME)?; + let description =3D reader.read_string_or_default(sizes::DESC); + let expression =3D reader.read_string_or_default(sizes::DESC); + + // No function pointer in packed struct + + Some(ConstraintSpec { + name, + description, + expression: opt_string(expression), + }) +} + +fn parse_signal(reader: &mut DataReader) -> Option { + // Matches `struct kapi_signal_spec`. All string fields are pointers. + let signal_num =3D reader.read_i32()?; + let signal_name =3D reader.read_optional_string(sizes::NAME).unwrap_or= _default(); + let direction =3D reader.read_u32()?; + let action =3D reader.read_u32()?; + let target =3D reader.read_optional_string(sizes::DESC); + let condition =3D reader.read_optional_string(sizes::DESC); + let description =3D reader.read_optional_string(sizes::DESC); + let restartable =3D reader.read_bool()?; + let sa_flags_required =3D reader.read_u32()?; + let sa_flags_forbidden =3D reader.read_u32()?; + let error_on_signal =3D reader.read_i32()?; + let transform_to =3D 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 =3D reader.read_optional_string(sizes::NAME).unwrap_or_= default(); + let timing =3D match timing_str.as_str() { + "KAPI_SIGNAL_TIME_BEFORE" | "before" =3D> 0u32, + "KAPI_SIGNAL_TIME_DURING" | "during" =3D> 1, + "KAPI_SIGNAL_TIME_AFTER" | "after" =3D> 2, + _ =3D> 0, + }; + let priority =3D reader.read_u8()?; + let interruptible =3D reader.read_bool()?; + let queue_behavior =3D reader.read_optional_string(sizes::NAME); + let state_required =3D reader.read_u32()?; + let state_forbidden =3D 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 !=3D 0 { + Some(error_on_signal) + } else { + None + }, + transform_to: if transform_to !=3D 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 { + let name =3D reader.read_optional_string(sizes::NAME)?; + let description =3D reader.read_string_or_default(sizes::DESC); + + // Skip signals array + for _ in 0..sizes::MAX_SIGNALS { + reader.read_i32(); + } + + let _signal_count =3D reader.read_u32()?; + + Some(SignalMaskSpec { name, description }) +} + +fn parse_struct_field(reader: &mut DataReader) -> Option { + let name =3D reader.read_optional_string(sizes::NAME)?; + let field_type =3D reader.read_u32()?; + let type_name =3D reader.read_optional_string(sizes::NAME)?; + let offset =3D reader.read_usize()?; + let size =3D reader.read_usize()?; + let flags =3D reader.read_u32()?; + let constraint_type =3D reader.read_u32()?; + let min_value =3D reader.read_i64()?; + let max_value =3D reader.read_i64()?; + let valid_mask =3D reader.read_u64()?; + // Skip enum_values field (512 bytes) + let _enum_values =3D reader.read_optional_string(sizes::DESC); // Don'= t fail on optional field + let description =3D 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 { + let name =3D reader.read_optional_string(sizes::NAME)?; + let size =3D reader.read_usize()?; + let alignment =3D reader.read_usize()?; + let field_count =3D reader.read_u32()?; + + // Parse fields array + let mut fields =3D Vec::new(); + for _ in 0..field_count.min(sizes::MAX_PARAMS as u32) { + if let Some(field) =3D parse_struct_field(reader) { + fields.push(field); + } else { + // Skip this field if we can't parse it + reader.skip(struct_field_layout_size()); + } + } + + // Skip remaining fields if any + let remaining =3D sizes::MAX_PARAMS as u32 - field_count.min(sizes::MA= X_PARAMS as u32); + for _ in 0..remaining { + reader.skip(struct_field_layout_size()); + } + + let description =3D reader.read_string_or_default(sizes::DESC); + + Some(StructSpec { + name, + size, + alignment, + field_count, + fields, + description, + }) +} + +fn parse_side_effect(reader: &mut DataReader) -> Option { + let effect_type =3D reader.read_u32()?; + let target =3D reader.read_optional_string(sizes::NAME)?; + let condition =3D reader.read_string_or_default(sizes::DESC); + let description =3D reader.read_string_or_default(sizes::DESC); + let reversible =3D reader.read_bool()?; + // No padding needed for packed struct + + Some(SideEffectSpec { + effect_type, + target, + condition: opt_string(condition), + description, + reversible, + }) +} + +fn parse_state_transition(reader: &mut DataReader) -> Option { + let from_state =3D reader.read_optional_string(sizes::NAME)?; + let to_state =3D reader.read_optional_string(sizes::NAME)?; + let condition =3D reader.read_string_or_default(sizes::DESC); + let object =3D reader.read_optional_string(sizes::NAME)?; + let description =3D 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 { + // 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 =3D reader.read_i32()?; + let cap_name =3D reader.read_optional_string(sizes::NAME)?; + let action =3D reader.read_u32()?; + let allows =3D reader.read_string_or_default(sizes::DESC); + let without_cap =3D reader.read_string_or_default(sizes::DESC); + let check_condition =3D reader.read_optional_string(sizes::DESC); + let priority =3D reader.read_u8()?; + + let mut alternatives =3D Vec::new(); + for _ in 0..sizes::MAX_CAPABILITIES { + if let Some(alt) =3D reader.read_i32() { + if alt !=3D 0 { + alternatives.push(alt); + } + } + } + + let _alternative_count =3D reader.read_u32()?; + + 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 =3D> "KAPI_CAP_BYPASS_CHECK", + 1 =3D> "KAPI_CAP_INCREASE_LIMIT", + 2 =3D> "KAPI_CAP_OVERRIDE_RESTRICTION", + 3 =3D> "KAPI_CAP_GRANT_PERMISSION", + 4 =3D> "KAPI_CAP_MODIFY_BEHAVIOR", + 5 =3D> "KAPI_CAP_ACCESS_RESOURCE", + 6 =3D> "KAPI_CAP_PERFORM_OPERATION", + _ =3D> return n.to_string(), + } + .to_string() +} diff --git a/tools/kapi/src/formatter/json.rs b/tools/kapi/src/formatter/js= on.rs new file mode 100644 index 0000000000000..ec1adfae0b448 --- /dev/null +++ b/tools/kapi/src/formatter/json.rs @@ -0,0 +1,634 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +use super::OutputFormatter; +use crate::extractor::{ + CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, Return= Spec, 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 =3D "Option::is_none")] + apis: Option>, + #[serde(skip_serializing_if =3D "Option::is_none")] + api_details: Option, +} + +#[derive(Serialize)] +struct JsonApi { + name: String, + api_type: String, +} + +#[derive(Serialize)] +struct JsonApiDetails { + name: String, + #[serde(skip_serializing_if =3D "Option::is_none")] + description: Option, + #[serde(skip_serializing_if =3D "Option::is_none")] + long_description: Option, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + context_flags: Vec, + #[serde(skip_serializing_if =3D "Option::is_none")] + examples: Option, + #[serde(skip_serializing_if =3D "Option::is_none")] + notes: Option, + // Sysfs-specific fields + #[serde(skip_serializing_if =3D "Option::is_none")] + subsystem: Option, + #[serde(skip_serializing_if =3D "Option::is_none")] + sysfs_path: Option, + #[serde(skip_serializing_if =3D "Option::is_none")] + permissions: Option, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + capabilities: Vec, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + state_transitions: Vec, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + side_effects: Vec, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + parameters: Vec, + #[serde(skip_serializing_if =3D "Option::is_none")] + return_spec: Option, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + errors: Vec, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + locks: Vec, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + struct_specs: Vec, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + signals: Vec, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + signal_masks: Vec, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + constraints: Vec, +} + +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 =3D 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 =3D 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) =3D &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::i= o::Result<()> { + Ok(()) + } + + fn begin_api_details(&mut self, _w: &mut dyn Write, name: &str) -> std= ::io::Result<()> { + self.data.api_details =3D 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) =3D &mut self.data.api_details { + details.description =3D Some(desc.to_string()); + } + Ok(()) + } + + fn long_description(&mut self, _w: &mut dyn Write, desc: &str) -> std:= :io::Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.long_description =3D Some(desc.to_string()); + } + Ok(()) + } + + fn begin_context_flags(&mut self, _w: &mut dyn Write) -> std::io::Resu= lt<()> { + Ok(()) + } + + fn context_flag(&mut self, _w: &mut dyn Write, flag: &str) -> std::io:= :Result<()> { + if let Some(details) =3D &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) =3D &mut self.data.api_details { + details.examples =3D Some(examples.to_string()); + } + Ok(()) + } + + fn notes(&mut self, _w: &mut dyn Write, notes: &str) -> std::io::Resul= t<()> { + if let Some(details) =3D &mut self.data.api_details { + details.notes =3D Some(notes.to_string()); + } + Ok(()) + } + + fn sysfs_subsystem(&mut self, _w: &mut dyn Write, subsystem: &str) -> = std::io::Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.subsystem =3D Some(subsystem.to_string()); + } + Ok(()) + } + + fn sysfs_path(&mut self, _w: &mut dyn Write, path: &str) -> std::io::R= esult<()> { + if let Some(details) =3D &mut self.data.api_details { + details.sysfs_path =3D Some(path.to_string()); + } + Ok(()) + } + + fn sysfs_permissions(&mut self, _w: &mut dyn Write, perms: &str) -> st= d::io::Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.permissions =3D Some(perms.to_string()); + } + Ok(()) + } + + fn begin_capabilities(&mut self, _w: &mut dyn Write) -> std::io::Resul= t<()> { + Ok(()) + } + + fn capability(&mut self, _w: &mut dyn Write, cap: &CapabilitySpec) -> = std::io::Result<()> { + if let Some(details) =3D &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) =3D &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) =3D &mut self.data.api_details { + details.return_spec =3D Some(ret.clone()); + } + Ok(()) + } + + fn error(&mut self, _w: &mut dyn Write, error: &ErrorSpec) -> std::io:= :Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.errors.push(error.clone()); + } + Ok(()) + } + + fn begin_signals(&mut self, _w: &mut dyn Write, _count: u32) -> std::i= o::Result<()> { + Ok(()) + } + + fn signal(&mut self, _w: &mut dyn Write, signal: &SignalSpec) -> std::= io::Result<()> { + if let Some(api_details) =3D &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) -> s= td::io::Result<()> { + Ok(()) + } + + fn signal_mask(&mut self, _w: &mut dyn Write, mask: &SignalMaskSpec) -= > std::io::Result<()> { + if let Some(api_details) =3D &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) -> s= td::io::Result<()> { + Ok(()) + } + + fn side_effect(&mut self, _w: &mut dyn Write, effect: &SideEffectSpec)= -> std::io::Result<()> { + if let Some(details) =3D &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) =3D &mut self.data.api_details { + details.state_transitions.push(trans.clone()); + } + Ok(()) + } + + fn end_state_transitions(&mut self, _w: &mut dyn Write) -> std::io::Re= sult<()> { + Ok(()) + } + + fn begin_constraints(&mut self, _w: &mut dyn Write, _count: u32) -> st= d::io::Result<()> { + Ok(()) + } + + fn constraint( + &mut self, + _w: &mut dyn Write, + constraint: &ConstraintSpec, + ) -> std::io::Result<()> { + if let Some(api_details) =3D &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::Re= sult<()> { + if let Some(details) =3D &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) -> s= td::io::Result<()> { + Ok(()) + } + + fn struct_spec(&mut self, _w: &mut dyn Write, spec: &StructSpec) -> st= d::io::Result<()> { + if let Some(ref mut details) =3D 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 =3D Vec::new(); + f.end_document(&mut buf).unwrap(); + String::from_utf8(buf).unwrap() + } + + #[test] + fn json_output_is_valid() { + let mut f =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + + // Verify it parses as valid JSON + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + 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 =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + + let apis =3D 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 =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + + // Must be valid JSON despite special characters + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + assert_eq!( + parsed["api_details"]["description"].as_str(), + Some("Contains \"quotes\" and \\backslashes\\") + ); + } + + #[test] + fn json_special_characters_in_name() { + let mut f =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + + // Must parse correctly; serde_json handles escaping for us + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + assert_eq!(parsed["apis"][0]["name"].as_str(), Some("sys_new\tline= ")); + } + + #[test] + fn json_parameters_serialized() { + let mut f =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + + let params =3D parsed["api_details"]["parameters"].as_array().unwr= ap(); + 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 =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + + let errors =3D 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 =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + + // description should not be present (skip_serializing_if =3D Opti= on::is_none) + assert!(parsed["api_details"]["description"].is_null()); + // parameters empty array should not be present (skip_serializing_= if =3D 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 =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + + let open_braces =3D json.chars().filter(|&c| c =3D=3D '{').count(); + let close_braces =3D json.chars().filter(|&c| c =3D=3D '}').count(= ); + assert_eq!(open_braces, close_braces, "Braces are unbalanced"); + + let open_brackets =3D json.chars().filter(|&c| c =3D=3D '[').count= (); + let close_brackets =3D json.chars().filter(|&c| c =3D=3D ']').coun= t(); + assert_eq!(open_brackets, close_brackets, "Brackets are unbalanced= "); + } + + #[test] + fn json_return_spec_serialized() { + let mut f =3D JsonFormatter::new(); + let mut sink =3D 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 =3D render_json(&mut f); + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + + let ret =3D &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 =3D JsonFormatter::new(); + let mut sink =3D 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{00e= a}") + .unwrap(); + f.end_api_details(&mut sink).unwrap(); + + let json =3D render_json(&mut f); + let parsed: serde_json::Value =3D serde_json::from_str(&json).unwr= ap(); + assert!(parsed["api_details"]["description"] + .as_str() + .unwrap() + .contains('\u{00e9}')); + } +} diff --git a/tools/kapi/src/formatter/mod.rs b/tools/kapi/src/formatter/mod= .rs new file mode 100644 index 0000000000000..362531af47102 --- /dev/null +++ b/tools/kapi/src/formatter/mod.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +use crate::extractor::{ + CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, Return= Spec, 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 =3D String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "plain" =3D> Ok(OutputFormat::Plain), + "json" =3D> Ok(OutputFormat::Json), + "rst" =3D> Ok(OutputFormat::Rst), + _ =3D> Err(format!("Unknown output format: {}", s)), + } + } +} + +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::i= o::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::R= esult<()>; + 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::Resul= t<()>; + 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) -> s= td::io::Result<()>; + fn sysfs_path(&mut self, w: &mut dyn Write, path: &str) -> std::io::Re= sult<()>; + 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) -> s= td::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::i= o::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::Res= ult<()>; + + // 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: &ConstraintSpe= c) + -> 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::R= esult<()>; + fn lock(&mut self, w: &mut dyn Write, lock: &LockSpec) -> std::io::Res= ult<()>; + 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 { + match format { + OutputFormat::Plain =3D> Box::new(PlainFormatter::new()), + OutputFormat::Json =3D> Box::new(JsonFormatter::new()), + OutputFormat::Rst =3D> Box::new(RstFormatter::new()), + } +} diff --git a/tools/kapi/src/formatter/plain.rs b/tools/kapi/src/formatter/p= lain.rs new file mode 100644 index 0000000000000..3b8a9e69e3b2a --- /dev/null +++ b/tools/kapi/src/formatter/plain.rs @@ -0,0 +1,646 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +use super::OutputFormatter; +use crate::extractor::{ + CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, Return= Spec, 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::i= o::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, "{}=3D", "=3D".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::R= esult<()> { + writeln!(w, "Description: {desc}") + } + + fn long_description(&mut self, w: &mut dyn Write, desc: &str) -> std::= io::Result<()> { + writeln!(w, "\nDetailed Description:")?; + writeln!(w, "{desc}") + } + + fn begin_context_flags(&mut self, w: &mut dyn Write) -> std::io::Resul= t<()> { + 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 mut flags =3D Vec::new(); + if param.flags & 0x01 !=3D 0 { + flags.push("IN"); + } + if param.flags & 0x02 !=3D 0 { + flags.push("OUT"); + } + if param.flags & 0x04 !=3D 0 { + flags.push("INOUT"); + } + if param.flags & 0x08 !=3D 0 { + flags.push("USER"); + } + if param.flags & 0x10 !=3D 0 { + flags.push("OPTIONAL"); + } + if !flags.is_empty() { + writeln!(w, " Flags: {}", flags.join(" | "))?; + } + + // Display constraints + if let Some(constraint) =3D ¶m.constraint { + writeln!(w, " Constraint: {constraint}")?; + } + if let (Some(min), Some(max)) =3D (param.min_value, param.max_valu= e) { + writeln!(w, " Range: {min} to {max}")?; + } + if let Some(mask) =3D 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) =3D ret.success_value { + writeln!(w, " Success value: {val}")?; + } + if let (Some(min), Some(max)) =3D (ret.success_min, ret.success_ma= x) { + 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:")?; + writeln!(w, "{examples}") + } + + fn notes(&mut self, w: &mut dyn Write, notes: &str) -> std::io::Result= <()> { + writeln!(w, "\nNotes:")?; + writeln!(w, "{notes}") + } + + fn sysfs_subsystem(&mut self, w: &mut dyn Write, subsystem: &str) -> s= td::io::Result<()> { + writeln!(w, "Subsystem: {subsystem}") + } + + fn sysfs_path(&mut self, w: &mut dyn Write, path: &str) -> std::io::Re= sult<()> { + 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) -> s= td::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) =3D &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::i= o::Result<()> { + write!(w, " {} ({})", signal.signal_name, signal.signal_num)?; + + // Display direction (bitmask matching C enum kapi_signal_directio= n) + let mut dirs =3D Vec::new(); + if signal.direction & 1 !=3D 0 { + dirs.push("RECEIVE"); + } + if signal.direction & 2 !=3D 0 { + dirs.push("SEND"); + } + if signal.direction & 4 !=3D 0 { + dirs.push("HANDLE"); + } + if signal.direction & 8 !=3D 0 { + dirs.push("BLOCK"); + } + if signal.direction & 16 !=3D 0 { + dirs.push("IGNORE"); + } + let direction =3D if dirs.is_empty() { + "UNKNOWN".to_string() + } else { + dirs.join("|") + }; + write!(w, " - {direction}")?; + + // Display action (matching C enum kapi_signal_action) + let action =3D match signal.action { + 0 =3D> "DEFAULT", + 1 =3D> "TERMINATE", + 2 =3D> "COREDUMP", + 3 =3D> "STOP", + 4 =3D> "CONTINUE", + 5 =3D> "CUSTOM", + 6 =3D> "RETURN", + 7 =3D> "RESTART", + 8 =3D> "QUEUE", + 9 =3D> "DISCARD", + 10 =3D> "TRANSFORM", + _ =3D> "UNKNOWN", + }; + writeln!(w, " - {action}")?; + + if let Some(target) =3D &signal.target { + writeln!(w, " Target: {target}")?; + } + if let Some(condition) =3D &signal.condition { + writeln!(w, " Condition: {condition}")?; + } + if let Some(desc) =3D &signal.description { + writeln!(w, " {desc}")?; + } + + // Display timing + let timing =3D match signal.timing { + 0 =3D> "BEFORE", + 1 =3D> "DURING", + 2 =3D> "AFTER", + 3 =3D> "EXIT", + _ =3D> "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) =3D &signal.queue { + writeln!(w, " Queue: {queue}")?; + } + if signal.sa_flags_required !=3D 0 { + writeln!( + w, + " SA flags required: {:#x}", + signal.sa_flags_required + )?; + } + if signal.sa_flags_forbidden !=3D 0 { + writeln!( + w, + " SA flags forbidden: {:#x}", + signal.sa_flags_forbidden + )?; + } + if signal.state_required !=3D 0 { + writeln!(w, " State required: {:#x}", signal.state_requir= ed)?; + } + if signal.state_forbidden !=3D 0 { + writeln!(w, " State forbidden: {:#x}", signal.state_forbi= dden)?; + } + if let Some(error) =3D signal.error_on_signal { + writeln!(w, " Error on signal: {error}")?; + } + if let Some(transform) =3D 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)?; + } + 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) =3D &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) =3D &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::Re= sult<()> { + 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) =3D &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::R= esult<()> { + writeln!(w, "\nLocking Requirements ({count}):") + } + + fn lock(&mut self, w: &mut dyn Write, lock: &LockSpec) -> std::io::Res= ult<()> { + write!(w, " {}", lock.lock_name)?; + + // Display lock type + let lock_type =3D match lock.lock_type { + 0 =3D> "NONE", + 1 =3D> "MUTEX", + 2 =3D> "SPINLOCK", + 3 =3D> "RWLOCK", + 4 =3D> "SEQLOCK", + 5 =3D> "RCU", + 6 =3D> "SEMAPHORE", + 7 =3D> "CUSTOM", + _ =3D> "UNKNOWN", + }; + writeln!(w, " ({lock_type})")?; + + let scope_str =3D match lock.scope { + 0 =3D> "acquired and released", + 1 =3D> "acquired (not released)", + 2 =3D> "released (held on entry)", + 3 =3D> "held by caller", + _ =3D> "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=3D{}, align=3D{}):", + 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_nam= e)?; + if !field.description.is_empty() { + write!(w, " {}", field.description)?; + } + writeln!(w)?; + + // Show constraints if present + if field.min_value !=3D 0 || field.max_value !=3D 0 { + writeln!( + w, + " Range: [{}, {}]", + field.min_value, field.max_value + )?; + } + if field.valid_mask !=3D 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) -> String { + f.end_document(sink).unwrap(); + String::from_utf8(sink.clone()).unwrap() + } + + #[test] + fn plain_api_list() { + let mut f =3D PlainFormatter::new(); + let mut sink =3D 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 =3D 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 =3D PlainFormatter::new(); + let mut sink =3D 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 =3D 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 =3D PlainFormatter::new(); + let mut sink =3D 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 =3D 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 =3D PlainFormatter::new(); + let mut sink =3D 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 =3D 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 =3D PlainFormatter::new(); + let mut sink =3D 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 =3D 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 =3D PlainFormatter::new(); + let mut sink =3D 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 =3D render_plain(&mut f, &mut sink); + assert!(out.contains("KAPI_CTX_PROCESS")); + assert!(out.contains("KAPI_CTX_SLEEPABLE")); + } +} diff --git a/tools/kapi/src/formatter/rst.rs b/tools/kapi/src/formatter/rst= .rs new file mode 100644 index 0000000000000..c4db74c9ad410 --- /dev/null +++ b/tools/kapi/src/formatter/rst.rs @@ -0,0 +1,726 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +use super::OutputFormatter; +use crate::extractor::{ + CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, Return= Spec, 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 =3D> '=3D', + 1 =3D> '-', + 2 =3D> '~', + 3 =3D> '^', + _ =3D> '"', + } + } +} + +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::i= o::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::R= esult<()> { + writeln!(w, "**{desc}**")?; + writeln!(w) + } + + fn long_description(&mut self, w: &mut dyn Write, desc: &str) -> std::= io::Result<()> { + writeln!(w, "{desc}")?; + writeln!(w) + } + + fn begin_context_flags(&mut self, w: &mut dyn Write) -> std::io::Resul= t<()> { + let title =3D "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 =3D 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 =3D 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 =3D "Examples"; + writeln!(w, "{title}")?; + writeln!( + w, + "{}", + Self::section_char(1).to_string().repeat(title.len()) + )?; + writeln!(w)?; + writeln!(w, ".. code-block:: c")?; + writeln!(w)?; + for line in examples.lines() { + writeln!(w, " {line}")?; + } + writeln!(w) + } + + fn notes(&mut self, w: &mut dyn Write, notes: &str) -> std::io::Result= <()> { + let title =3D "Notes"; + writeln!(w, "{title}")?; + writeln!( + w, + "{}", + Self::section_char(1).to_string().repeat(title.len()) + )?; + writeln!(w)?; + writeln!(w, "{notes}")?; + writeln!(w) + } + + fn sysfs_subsystem(&mut self, w: &mut dyn Write, subsystem: &str) -> s= td::io::Result<()> { + writeln!(w, ":Subsystem: {subsystem}")?; + writeln!(w) + } + + fn sysfs_path(&mut self, w: &mut dyn Write, path: &str) -> std::io::Re= sult<()> { + 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 =3D "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) -> s= td::io::Result<()> { + writeln!(w, "**{} ({})** - {}", cap.name, cap.capability, cap.acti= on)?; + 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) =3D &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 mut flags =3D Vec::new(); + if param.flags & 0x01 !=3D 0 { + flags.push("IN"); + } + if param.flags & 0x02 !=3D 0 { + flags.push("OUT"); + } + if param.flags & 0x04 !=3D 0 { + flags.push("USER"); + } + if param.flags & 0x08 !=3D 0 { + flags.push("OPTIONAL"); + } + if !flags.is_empty() { + writeln!(w, " :Flags: {}", flags.join(", "))?; + } + + if let Some(constraint) =3D ¶m.constraint { + writeln!(w, " :Constraint: {}", constraint)?; + } + + if let (Some(min), Some(max)) =3D (param.min_value, param.max_valu= e) { + writeln!(w, " :Range: {} to {}", min, max)?; + } + + 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) =3D 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 =3D 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::i= o::Result<()> { + write!(w, "* **{}**", signal.signal_name)?; + if signal.signal_num !=3D 0 { + write!(w, " ({})", signal.signal_num)?; + } + writeln!(w)?; + + // Direction (bitmask matching C enum kapi_signal_direction) + let mut dirs =3D Vec::new(); + if signal.direction & 1 !=3D 0 { + dirs.push("receive"); + } + if signal.direction & 2 !=3D 0 { + dirs.push("send"); + } + if signal.direction & 4 !=3D 0 { + dirs.push("handle"); + } + if signal.direction & 8 !=3D 0 { + dirs.push("block"); + } + if signal.direction & 16 !=3D 0 { + dirs.push("ignore"); + } + let direction =3D if dirs.is_empty() { + "unknown".to_string() + } else { + dirs.join(", ") + }; + writeln!(w, " :Direction: {}", direction)?; + + // Action (matching C enum kapi_signal_action) + let action =3D match signal.action { + 0 =3D> "default", + 1 =3D> "terminate", + 2 =3D> "coredump", + 3 =3D> "stop", + 4 =3D> "continue", + 5 =3D> "custom", + 6 =3D> "return", + 7 =3D> "restart", + 8 =3D> "queue", + 9 =3D> "discard", + 10 =3D> "transform", + _ =3D> "unknown", + }; + writeln!(w, " :Action: {}", action)?; + + if let Some(target) =3D &signal.target { + writeln!(w, " :Target: {}", target)?; + } + if let Some(cond) =3D &signal.condition { + writeln!(w, " :Condition: {}", cond)?; + } + if let Some(desc) =3D &signal.description { + writeln!(w, " :Description: {}", desc)?; + } + let timing =3D match signal.timing { + 0 =3D> "before", + 1 =3D> "during", + 2 =3D> "after", + 3 =3D> "exit", + _ =3D> "", + }; + if !timing.is_empty() { + writeln!(w, " :Timing: {}", timing)?; + } + if signal.priority !=3D 0 { + writeln!(w, " :Priority: {}", signal.priority)?; + } + if signal.interruptible { + writeln!(w, " :Interruptible: yes")?; + } + if signal.restartable { + writeln!(w, " :Restartable: yes")?; + } + if let Some(queue) =3D &signal.queue { + writeln!(w, " :Queue: {}", queue)?; + } + if signal.sa_flags_required !=3D 0 { + writeln!(w, " :SA flags required: {:#x}", signal.sa_flags_req= uired)?; + } + if signal.sa_flags_forbidden !=3D 0 { + writeln!(w, " :SA flags forbidden: {:#x}", signal.sa_flags_fo= rbidden)?; + } + if signal.state_required !=3D 0 { + writeln!(w, " :State required: {:#x}", signal.state_required)= ?; + } + if signal.state_forbidden !=3D 0 { + writeln!(w, " :State forbidden: {:#x}", signal.state_forbidde= n)?; + } + if let Some(error) =3D signal.error_on_signal { + writeln!(w, " :Error on signal: {}", error)?; + } + if let Some(transform) =3D 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 =3D 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)?; + } + 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 =3D 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) =3D &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 =3D 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, + "* **{}**: {} =E2=86=92 {}", + trans.object, trans.from_state, trans.to_state + )?; + writeln!(w, " {}", trans.description)?; + if let Some(cond) =3D &trans.condition { + writeln!(w, " :Condition: {}", cond)?; + } + writeln!(w) + } + + fn end_state_transitions(&mut self, _w: &mut dyn Write) -> std::io::Re= sult<()> { + Ok(()) + } + + fn begin_constraints(&mut self, w: &mut dyn Write, count: u32) -> std:= :io::Result<()> { + let title =3D 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) =3D &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::R= esult<()> { + let title =3D 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::Res= ult<()> { + write!(w, "* **{}**", lock.lock_name)?; + let lock_type_str =3D match lock.lock_type { + 1 =3D> " *(mutex)*", + 2 =3D> " *(spinlock)*", + 3 =3D> " *(rwlock)*", + 4 =3D> " *(semaphore)*", + 5 =3D> " *(RCU)*", + _ =3D> "", + }; + 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) -> st= d::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 !=3D 0 || field.max_value !=3D 0 { + writeln!(w, " Range: [{}, {}]", field.min_value, fi= eld.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, ParamSpec, ReturnSpec}; + + fn render_rst(f: &mut RstFormatter, sink: &mut Vec) -> String { + f.end_document(sink).unwrap(); + String::from_utf8(sink.clone()).unwrap() + } + + #[test] + fn rst_api_details_has_heading() { + let mut f =3D RstFormatter::new(); + let mut sink =3D 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 =3D render_rst(&mut f, &mut sink); + assert!(out.contains("sys_test")); + assert!(out.contains("=3D=3D=3D=3D=3D=3D=3D=3D")); + assert!(out.contains("**A test syscall**")); + } + + #[test] + fn rst_api_list() { + let mut f =3D RstFormatter::new(); + let mut sink =3D 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 =3D render_rst(&mut f, &mut sink); + assert!(out.contains("sys_open")); + assert!(out.contains("sys_read")); + } + + #[test] + fn rst_parameters() { + let mut f =3D RstFormatter::new(); + let mut sink =3D 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 =3D 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 =3D RstFormatter::new(); + let mut sink =3D 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 =3D 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 =3D RstFormatter::new(); + let mut sink =3D 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 =3D 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 =3D RstFormatter::new(); + let mut sink =3D 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 =3D render_rst(&mut f, &mut sink); + assert!(out.contains("KAPI_CTX_PROCESS")); + assert!(out.contains("KAPI_CTX_SLEEPABLE")); + assert!(out.contains("Execution Context")); + } +} diff --git a/tools/kapi/src/main.rs b/tools/kapi/src/main.rs new file mode 100644 index 0000000000000..29b76a42f26ab --- /dev/null +++ b/tools/kapi/src/main.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin + +//! kapi - Kernel API Specification Tool +//! +//! This tool extracts and displays kernel API specifications from multipl= e sources: +//! - Kernel source code (KAPI macros) +//! - 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, VmlinuxEx= tractor}; +use formatter::{create_formatter, OutputFormat}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about =3D None)] +struct Args { + /// Path to the vmlinux file + #[arg(long, value_name =3D "PATH", group =3D "input")] + vmlinux: Option, + + /// Path to kernel source directory or file + #[arg(long, value_name =3D "PATH", group =3D "input")] + source: Option, + + /// Path to debugfs (defaults to /sys/kernel/debug if not specified) + #[arg(long, value_name =3D "PATH", group =3D "input")] + debugfs: Option, + + /// Optional: Name of specific API to show details for + api_name: Option, + + /// Output format + #[arg(long, short =3D 'f', default_value =3D "plain")] + format: String, +} + +fn main() -> Result<()> { + let args =3D Args::parse(); + + let output_format: OutputFormat =3D args + .format + .parse() + .map_err(|e: String| anyhow::anyhow!(e))?; + + let extractor: Box =3D match (&args.vmlinux, &args.s= ource, &args.debugfs) { + (Some(vmlinux_path), None, None) =3D> Box::new(VmlinuxExtractor::n= ew(vmlinux_path)?), + (None, Some(source_path), None) =3D> Box::new(SourceExtractor::new= (source_path)?), + (None, None, Some(_) | None) =3D> { + // If debugfs is specified or no input is provided, use debugfs + Box::new(DebugfsExtractor::new(args.debugfs.clone())?) + } + _ =3D> { + 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, + output_format: OutputFormat, +) -> Result<()> { + let mut formatter =3D create_formatter(output_format); + let mut stdout =3D io::stdout(); + + formatter.begin_document(&mut stdout)?; + + if let Some(api_name_req) =3D api_name { + // Use the extractor to display API details + if let Some(_spec) =3D 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 =3D=3D 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 =3D extractor.extract_all()?; + + // Helper to display API list for a specific type + let mut display_api_type =3D |api_type: &str, title: &str| -> Resu= lt<()> { + let filtered: Vec<_> =3D all_specs + .iter() + .filter(|s| s.api_type =3D=3D 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(()) +} --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 50FD53C1418; Fri, 29 May 2026 23:33:30 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097614; cv=none; b=IYEUJTNu1HK26UcV/N2lH/jW9KpYeZ/hi4MYjMnyBD3CWUcog5t8IxHPjX0OWp384sPHqE1q/gFItvJikS6Zz9r0lwDpPZU90V3JKfOOFbxxXME80zjxEP5cWVq8ZXDsiq2ZOjFB6YeFHQun1txPmuSgBHlpfEbCVjGXtqtcDqs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097614; c=relaxed/simple; bh=M1Pjct4l36TWNUmH8oDG2tYRqelUXhS+LpmdVklvqfU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=MHzFwpqppIwYzPNeozMz83gRF8hwu7Mlwixveb//o6R7X+spTzTUHtj5kTKr/Hh/vCvneyP2vurCxxhi/+2AM/xUppBuKlvsbWaCmllTClTXkFaJlF945HRpvdqTRW6SW3iDwDvQq3HVd8/KfdfbUd7IMED+k2LOQjjK3ryVho8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=oLPYNgAA; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="oLPYNgAA" Received: by smtp.kernel.org (Postfix) with ESMTPSA id F06AC1F0089E; Fri, 29 May 2026 23:33:27 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097610; bh=2JTxvlFGHAdWOihq8FCQiAMMDV6W6dDMe4D1Ktr4ZzM=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=oLPYNgAAnZ6FstcAslx8XVjvkOWCZuULld5KwPzTiMtf+McwomZOnK4GOstufCkEU 7quSCcGLR0xVhFqz+s0n2TAtjf/ZFESYEcJz7s2JthKZq+Orxl1CC84C3ieppVlFD7 HvHlI3Uyt92MW73M0ORPGtJ3sVsTu2omJ/g2+PfWM/FUaCTj9i9bw796Xq5rsANFP8 RZWVrTIglK5+Ytf9UlWIWKRtONOIGUiVB8VnD9WsRdIlCrJeOBb+GMzB0ZvWt1lDso Cor0eAz+j1kDN/Sa5dmOL62V2CMXixoVDK0GRkm7I6QfN/CFum6AY9S4ZL0iCJzOFH ifrG5EjXlxhSw== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 05/11] kernel/api: add API specification for sys_open Date: Fri, 29 May 2026 19:33:04 -0400 Message-ID: <20260529233311.1901670-6-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add KAPI-annotated kerneldoc for the sys_open system call in fs/open.c. The specification documents parameter constraints (pathname, flags bitmask, permission mode), 25 error conditions, locking requirements, side effects, required capabilities, and usage examples. Signed-off-by: Sasha Levin --- fs/open.c | 317 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) diff --git a/fs/open.c b/fs/open.c index 91f1139591abe..61573d5abda00 100644 --- a/fs/open.c +++ b/fs/open.c @@ -1373,6 +1373,323 @@ int do_sys_open(int dfd, const char __user *filenam= e, int flags, umode_t mode) } =20 =20 +/** + * 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 specified by pathname. If O_CREAT or O_TMPFIL= E is + * specified in flags, the file is created if it does not exist; its mod= e is + * set according to the mode parameter modified by the process's umask. + * + * The flags argument must include one of the following access modes: O_= RDONLY + * (read-only), O_WRONLY (write-only), or O_RDWR (read/write). These are= the + * low-order two bits of flags. In addition, zero or more file creation = and + * file status flags can be bitwise-ORed in flags. + * + * File creation flags: O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC, O_DIRECTORY, + * O_NOFOLLOW, O_CLOEXEC, O_TMPFILE. These flags affect open behavior. + * + * 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 o= f the + * file's open file description and can be retrieved/modified with fcntl= (). + * + * The return value is a file descriptor, a small nonnegative integer us= ed in + * subsequent system calls (read, write, lseek, fcntl, etc.) to refer to= the + * open file. The file descriptor returned by a successful open is the l= owest- + * numbered file descriptor not currently open for the process. + * + * On 64-bit systems, O_LARGEFILE is automatically added to the flags. O= n 32-bit + * systems, files larger than 2GB require O_LARGEFILE to be explicitly s= et. + * + * This syscall is a legacy interface. Modern code should prefer openat(= ) for + * relative path operations and openat2() for additional control via res= olve + * flags. The open() call is equivalent to openat(AT_FDCWD, pathname, fl= ags). + * + * 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 termina= tor. + * For relative paths, resolution starts from current working director= y. + * The path is followed (symlinks resolved) unless O_NOFOLLOW is speci= fied. + * + * 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_DSYNC | O_S= YNC | FASYNC | + * O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLL= OW | O_NOATIME | + * O_CLOEXEC | O_PATH | O_TMPFILE) + * cdesc: Must include exactly one of O_RDONLY (0), O_WRONLY (1), or + * O_RDWR (2) as the access mode. Additional flags may be ORed. Invali= d flag + * combinations (e.g., O_PATH with incompatible flags, O_TMPFILE witho= ut + * O_DIRECTORY, O_TMPFILE with read-only mode) return EINVAL. Since Li= nux + * 6.7, O_CREAT is silently ignored when combined with O_DIRECTORY. Un= known + * flags are silently ignored for backward compatibility (unlike opena= t2 + * which rejects them). + * + * param: mode + * type: uint, input + * constraint-type: mask(S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG= | S_IRWXO) + * 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 & ~umas= k). + * When O_CREAT/O_TMPFILE is not set, mode is ignored. Mode values exc= eeding + * S_IALLUGO (07777) are masked off. + * + * return: + * type: int + * check-type: fd + * success: >=3D 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 perm= ission + * 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 + * the file is on a filesystem mounted with noexec and MAY_EXEC was im= plied. + * + * error: EAGAIN, Resource temporarily unavailable + * desc: The file is a FIFO or regular file, O_NONBLOCK is specified, an= d the + * operation would block. Also returned when RESOLVE_CACHED is used wi= th + * openat2() and the lookup cannot be satisfied from the dentry cache. + * + * error: EBUSY, Device or resource busy + * desc: O_EXCL was specified in flags and pathname refers to a block de= vice + * 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 alread= y exists. + * This error is atomic with respect to file creation - it prevents ra= ce + * 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: The call was interrupted by a signal handler before completing = file + * open. This can occur during lock acquisition or when breaking lease= s. + * + * error: EINVAL, Invalid argument + * desc: Returned for several conditions: (1) Invalid O_* flag combinati= ons + * (O_TMPFILE without O_DIRECTORY, O_TMPFILE with read-only access, O_= PATH + * with flags other than O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC). + * (2) mode contains bits outside S_IALLUGO when O_CREAT/O_TMPFILE + * is set (openat2 only). (3) O_DIRECT requested but filesystem doesn't + * support it. (4) The filesystem does not support O_SYNC or O_DSYNC. + * + * 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_TMPFIL= E is + * used on a directory that doesn't support tmpfile operations. + * + * 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. + * + * error: EMFILE, Too many open files + * desc: The per-process limit on the number of open file descriptors ha= s been + * reached. This limit is RLIMIT_NOFILE (default typically 1024, max s= et by + * /proc/sys/fs/nr_open). + * + * error: ENAMETOOLONG, File name too long + * desc: pathname was too long, exceeding PATH_MAX (4096) bytes, or a si= ngle + * 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 e= xceed + * this limit. + * + * error: ENODEV, No such device + * desc: pathname refers to a special file that has no corresponding dev= ice, or + * the file's inode has no file operations assigned. + * + * error: ENOENT, No such file or directory + * desc: A directory component in pathname does not exist or is a dangli= ng + * symbolic link, or O_CREAT is not set and the named file does not ex= ist, + * or pathname is an empty string (unless AT_EMPTY_PATH is used with o= penat2). + * + * error: ENOMEM, Out of memory + * desc: The kernel could not allocate sufficient memory for the file st= ructure, + * 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 dire= ctory + * 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 d= irectory, + * 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 that does not exist. + * + * 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 opene= d. + * 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 calle= r did + * not match the owner of the file and the caller is not privileged, o= r 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 operatio= n. + * + * error: EROFS, Read-only file system + * desc: pathname refers to a file on a read-only filesystem and write a= ccess + * was requested. + * + * error: ETXTBSY, Text file busy + * desc: pathname refers to an executable image which is currently being + * executed, or to a swap file, and write access or truncation was req= uested. + * + * error: EWOULDBLOCK, Resource temporarily unavailable + * desc: O_NONBLOCK was specified and an incompatible lease is held on t= he file. + * + * lock: files->file_lock + * type: spinlock + * acquired: true + * released: true + * desc: Acquired when allocating a file descriptor slot. Held briefly d= uring + * fd allocation via alloc_fd() and released before the syscall return= s. + * + * lock: inode->i_rwsem (parent directory) + * type: rwlock + * acquired: conditional + * released: true + * desc: Write lock acquired on parent directory inode when creating a n= ew file + * (O_CREAT). Acquired via inode_lock_nested() in lookup path. May use + * killable variant which can return EINTR on fatal signal. + * + * lock: RCU read-side + * type: rcu + * acquired: true + * released: true + * desc: Path lookup uses RCU mode initially for performance. If RCU loo= kup + * fails (returns -ECHILD), falls back to reference-based lookup. + * + * signal: Any signal + * direction: receive + * action: return + * condition: When blocked on interruptible or killable operations + * desc: The syscall may be interrupted during path lookup, lock acquisi= tion, + * or lease breaking. Fatal signals (SIGKILL, etc.) will interrupt kil= lable + * operations. Non-fatal signals may interrupt interruptible operation= s. + * 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. Allo= cates + * a struct file from the filp slab cache. May allocate dentries and i= nodes + * during path lookup. System-wide file count (nr_files) is incremente= d. + * 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, alloca= tes + * 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. Updat= es + * file mtime and ctime. May trigger notifications to lease holders. + * reversible: no + * + * side-effect: modify_state + * target: inode timestamps + * condition: Unless O_NOATIME is specified + * desc: Opens for reading may update inode access time (atime) unless m= ounted + * with noatime/relatime or O_NOATIME is specified. Opens for writing = that + * truncate or create 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 appli= ed + * 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 dire= ctories + * without: Must have read permission on file or search permission on di= rectory + * 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 >=3D 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 controll= ed + * 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. Proce= sses + * 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 =3D open("/etc/passwd", O_RDONLY); // Read existing file + * fd =3D open("/tmp/newfile", O_WRONLY | O_CREAT | O_TRUNC, 0644); // = Create/truncate + * fd =3D open("/tmp/lockfile", O_WRONLY | O_CREAT | O_EXCL, 0600); // = Exclusive create + * fd =3D open("/dev/null", O_RDWR); // Open device + * fd =3D open("/tmp", O_RDONLY | O_DIRECTORY); // Open directory + * fd =3D open("/tmp", O_TMPFILE | O_RDWR, 0600); // Anonymous temp file + * + * notes: The distinction between O_RDONLY, O_WRONLY, and O_RDWR is critic= al. + * O_RDONLY is defined as 0, so (flags & O_RDONLY) always evaluates to z= ero. + * Test access mode using (flags & O_ACCMODE) =3D=3D O_RDONLY. + * + * When O_CREAT is specified without O_EXCL, there is a race condition b= etween + * 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 de= scriptor + * leaks to child processes between fork() and execve(). + * + * O_DIRECT has alignment requirements that vary by filesystem. Use stat= x() + * 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 oper= ations + * (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()) --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E8BAC3AD520; Fri, 29 May 2026 23:33:33 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097616; cv=none; b=S6I0AJxq5I13OzreztDUyqim1gv1jhvOzb19cVUMHBijQhMIo8F8iZrDn5wq3V4QDK502Ps2miaWAarpX0k6M6nM7m8rwVfUCPk8XoF4TzN4w218mc2dgMl8U9EdAUfHt06yypNqlFm7F3hbdOUf7lXVIx+IynYIWQs41+MYFvo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097616; c=relaxed/simple; bh=BXuton25+wrRyNf6kcvAwZfC9LtOjTxpIBFNGQhiigE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=qtWw5pJrKl2dsL3nV8kXRo4pB++unTodCK/48G1NrMEhtHIFyUjhSe3SYW9VO03MDLjErEdenz1TzZeAV9A2ojLoFgk2xnMf5+g29123z2mi4fm8VqiSPHMDuzq1QuLK3MvDoS5oYSfZsYR3IOPazpDkgSdirLANup7NV4H3vhA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=kgxEe0gX; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="kgxEe0gX" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 082241F00893; Fri, 29 May 2026 23:33:30 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097613; bh=3CKngsHKDWiJtDMO/YQEWNXIkDdDnJnlTzZ2AENHZK0=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=kgxEe0gXH9saWo+Iu6DFSfLSqFV57LdMxj55kufvGFw3nw/CPjt/M7B6wHHb7/kNb 77u3A7VfIZJaARkIOgaESRqhZzEuyGM7LkesvQOsgL/M9+o9XZlvztA9B+fUoHzKJZ RuW9KEgkVUvSsAgRBlQvAF/TyVg9q95U0IMkLPon+lojjd6XTvVUaTFeIgBJgiPapJ z0A48LLuim5nwnATWR4mIJ7HQogrwsfK2LsoSa/foIxbJARaJKSMi+cFMaqbxPcQQ9 pnF6gSX0P8Ls4TL6Jt3tOb1w5l2AUTQgzhmleKys02AiI6zLRVYo9NWjcjcc0EU9q4 8FUZBD42bgQVA== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 06/11] kernel/api: add API specification for sys_close Date: Fri, 29 May 2026 19:33:05 -0400 Message-ID: <20260529233311.1901670-7-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add KAPI-annotated kerneldoc for the sys_close system call in fs/open.c. The specification documents the file descriptor parameter, error conditions, locking requirements, side effects on pending I/O, and the close-on-exec relationship. Signed-off-by: Sasha Levin --- fs/open.c | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/fs/open.c b/fs/open.c index 61573d5abda00..24f6bd5e50090 100644 --- a/fs/open.c +++ b/fs/open.c @@ -1808,6 +1808,250 @@ EXPORT_SYMBOL(filp_close); * releasing the fd. This ensures that one clone task can't release * an fd while another clone is opening it. */ +/** + * 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 (note: POSIX locks are released when ANY= fd + * for that inode is closed by the process, not just the last one). OFD = locks + * and flock locks are associated with the open file description and are= only + * released when the last file descriptor referring to that open file + * description is closed. + * When this is the last file descriptor + * referring to the underlying open file description, associated resourc= es are + * freed. If the file was previously unlinked, the file itself is delete= d when + * the last reference is closed. + * + * CRITICAL: The file descriptor is ALWAYS closed, even when close() ret= urns + * an error. This differs from POSIX semantics where the state of the fi= le + * descriptor is unspecified after EINTR. On Linux, the fd is released e= arly + * in close() processing before flush operations that may fail. Therefor= e, + * retrying close() after an error return is DANGEROUS and may close an + * unrelated file descriptor that was assigned to another thread. + * + * Errors returned from close() (EIO, ENOSPC, EDQUOT) indicate that the = final + * flush of buffered data failed. These errors commonly occur on network + * filesystems like NFS when write errors are deferred to close time. A + * successful return from close() does NOT guarantee that data has been + * successfully written to disk; the kernel uses buffer cache to defer w= rites. + * To ensure data persistence, call fsync() before close(). + * + * Note that close() does not affect the close-on-exec flag set via + * fcntl(fd, F_SETFD, FD_CLOEXEC) or O_CLOEXEC on other file descriptors. + * The close-on-exec flag only causes automatic close during exec(), not + * during explicit close() calls. + * + * On close, the following cleanup operations are performed: POSIX advis= ory + * locks are removed, dnotify registrations are cleaned up, the file is + * flushed to storage if applicable, and the file + * reference is released. If this was the last reference, additional cle= anup + * includes: fsnotify close notification, epoll cleanup, flock and lease + * removal, FASYNC cleanup, 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, but values exceeding INT_MAX are ef= fectively + * invalid due to internal checks. + * + * return: + * type: int + * check-type: exact + * success: 0 + * desc: Returns 0 on success. On error, returns a negative error code. + * IMPORTANT: Even when an error is returned, the file descriptor is s= till + * closed and must not be used again. The error indicates a problem wi= th + * the final flush operation, not that the fd remains open. + * + * error: EBADF, Bad file descriptor + * desc: The file descriptor fd is not a valid open file descriptor, or = was + * already closed. This is the only error that indicates the fd was NOT + * closed (because it was never open to begin with). Occurs when fd is= out + * of range, has no file assigned, or was already closed. + * + * error: EINTR, Interrupted system call + * desc: The flush operation was interrupted by a signal before completi= on. + * This occurs when the close-time flush operation (e.g., on NFS) perf= orms an + * interruptible wait that receives a signal. IMPORTANT: Despite this = error, + * the file descriptor IS closed and must not be used again. This error + * is generated by converting kernel-internal restart codes (ERESTARTS= YS, + * ERESTARTNOINTR, ERESTARTNOHAND, ERESTART_RESTARTBLOCK) to EINTR bec= ause + * restarting the syscall would be incorrect once the fd is freed. + * + * error: EIO, I/O error + * desc: An I/O error occurred during the flush of buffered data to the + * underlying storage. This typically indicates a hardware error, netw= ork + * failure on NFS, or other storage system error. The file descriptor = is + * still closed. Previously buffered write data may have been lost. + * + * error: ENOSPC, No space left on device + * desc: There was insufficient space on the storage device to flush buf= fered + * writes. This is common on NFS when the server runs out of space bet= ween + * write() and close(). The file descriptor is still closed. + * + * error: EDQUOT, Disk quota exceeded + * desc: The user's disk quota was exceeded while attempting to flush bu= ffered + * writes. Common on NFS when quota is exceeded between write() and cl= ose(). + * The file descriptor is still closed. + * + * lock: files->file_lock + * type: spinlock + * acquired: true + * released: true + * desc: Acquired via file_close_fd() to atomically lookup and remove th= e fd + * from the file descriptor table. Held only during the table manipula= tion; + * released before flush and final cleanup operations. This ensures th= at + * another thread cannot allocate the same fd number while close is in + * progress. + * + * lock: file->f_lock + * type: spinlock + * acquired: true + * released: true + * desc: Acquired during epoll cleanup (eventpoll_release_file) and dnot= ify + * cleanup to safely unlink the file from monitoring structures. May a= lso + * be acquired during lock context operations. + * + * lock: ep->mtx + * type: mutex + * acquired: true + * released: true + * desc: Acquired during epoll cleanup if the file was monitored by epol= l. + * Used to safely remove the file from epoll interest lists. + * + * lock: flc_lock + * type: spinlock + * acquired: true + * released: true + * desc: File lock context spinlock, acquired during locks_remove_file()= to + * safely remove POSIX, flock, and lease locks associated with the fil= e. + * + * signal: pending_signals + * direction: receive + * action: return + * condition: When close-time flush performs interruptible wait + * desc: If the close-time flush operation (e.g., on NFS) 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 descript= or + * table, making the fd number available for reuse by subsequent open(= ), + * dup(), or similar calls. This occurs BEFORE any flush or cleanup th= at + * might fail, making the operation irreversible regardless of return = value. + * condition: Always (when fd is valid) + * reversible: no + * + * side-effect: lock_release + * target: POSIX advisory locks, OFD locks, flock locks + * desc: All advisory locks held on the file by this process are removed. + * POSIX locks are removed via locks_remove_posix() during filp_flush(= ). + * All lock types (POSIX, OFD, flock) are removed via locks_remove_fil= e() + * during __fput() when this is the last reference. + * condition: File has FMODE_OPENED and !(FMODE_PATH) + * reversible: no + * + * side-effect: resource_destroy + * target: File leases + * desc: Any file leases held on the file are removed during locks_remov= e_file() + * when this is the last reference to the open file description. + * condition: File had leases and this is the last close + * 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 direc= tories. + * 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 remov= ed + * from those interest lists via eventpoll_release(). + * condition: File was added to epoll instances + * reversible: no + * + * side-effect: filesystem + * target: Buffered data + * desc: Any buffered data is flushed if applicable (e.g., on NFS). This + * attempts to write any buffered data to storage + * and may return errors (EIO, ENOSPC, EDQUOT) if the flush fails. The + * success of this flush is NOT guaranteed even with a 0 return; use + * fsync() before close() to ensure data persistence. + * condition: File was opened for writing and has buffered data + * 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, cl= osing + * the last reference causes the actual file data to be removed from t= he + * 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 (i= nvalid). + * The fd number becomes available for reuse. This transition occurs e= arly + * in close() processing, before any operations that might fail. + * + * 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. + * + * constraint: File Descriptor Reuse Race + * desc: Because the fd is freed early in close() processing, another th= read + * may receive the same fd number from a concurrent open() before clos= e() + * returns. Applications must not retry close() after an error return,= as + * this could close an unrelated file opened by another thread. + * expr: After close(fd) returns (even with error), fd is invalid + * + * examples: close(fd); // Basic usage - ignore errors (common but not id= eal) + * if (close(fd) =3D=3D -1) perror("close"); // Log errors for debugging + * fsync(fd); close(fd); // Ensure data persistence before closing + * + * notes: The fd is always freed regardless of the return value. POSIX + * specifies that on EINTR the state of the fd is unspecified, but Linux + * always closes it. Retrying close() after an error may close an unrela= ted + * fd that was reassigned by another thread, so callers should never ret= ry. + * + * Error codes like EIO, ENOSPC, and EDQUOT indicate that previously buf= fered + * writes may have failed to reach storage. These errors are particularly + * common on NFS where write errors are often deferred to close time. + * + * 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. + */ SYSCALL_DEFINE1(close, unsigned int, fd) { int retval; --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 081463C4175; Fri, 29 May 2026 23:33:37 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097619; cv=none; b=JOAimEJrvb2Sn9Ie3XW/9BySQXPl3HO+v69K4joYKIM15MNN+EKEuCaB70Hy8M8tQ595+wM37OSSpsc79qcaOEiYTmtRlucoK5MTC8ujPXk/umnaTs6ybKmBo5v9anvIQx37kLO2LtpavT/BB64Nc7foVTlOZsbOcbWkfSn1Z04= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097619; c=relaxed/simple; bh=2rHH9RkBPAdA1LYbLNeJGK8f6QyepHAx4OuQGgfk0Lk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=nXU1eTyZIRv5kQarWbDP2KjeCtJO3Emhx+0bnW+uj+xoZuUNPA3DR0KJfQcntNT5Mk4K3TocA2r3W87/1ts/K2+VpTun1SYqbxGEW02AD4hXNaGGbaPjP8kPg7jrQqHnxtVG+ZtCbcQUmxZ1v70QHPRCDrKCB8t9P8oCZ12rchM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=KuaWe6wP; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="KuaWe6wP" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 15EF51F00899; Fri, 29 May 2026 23:33:34 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097616; bh=E3u9cvFswo9c3xL9UMepXjBERlNyUcSduDWuYNWUIQo=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=KuaWe6wPOoM6kU+gAgsr7tlcYh8hRGSMOtAmMqIksn1nOcw5Is9XdslsVmaxBbUif wQr8obhrl4VEwz6IftZoV2tZZyNCtsrp0OJK6SCnT5Ny5p5dGoKyAGVlymn6ItewVU HXru2AVN/XG/8ldWTVUYfzbtATdVEpELLL91MqJAP3RI3U4U+MRbPt1xhHUzv55xuE hfcU4Ufd61YMPTwy/uautmgNQvujn6Y8wcKRyB7A6bDnoQrCtFc/e3UlgBqtZlnXid Mw44EDkw2fXGwUIPXIKqvpRsXyTPCb27IDK0dPkNYKju3IGbStYmTNEZ+F+v+qCIi3 A4W6eV90UiG3w== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 07/11] kernel/api: add API specification for sys_read Date: Fri, 29 May 2026 19:33:06 -0400 Message-ID: <20260529233311.1901670-8-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add KAPI-annotated kerneldoc for the sys_read system call in fs/read_write.c. The specification documents parameter constraints (fd, user buffer, count), error conditions, locking requirements, signal handling behavior, and short read semantics. Signed-off-by: Sasha Levin --- fs/read_write.c | 301 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) diff --git a/fs/read_write.c b/fs/read_write.c index 50bff7edc91f3..1a5dd11a0f3ed 100644 --- a/fs/read_write.c +++ b/fs/read_write.c @@ -721,6 +721,307 @@ ssize_t ksys_read(unsigned int fd, char __user *buf, = size_t count) return ret; } =20 +/** + * 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 i= nto + * the buffer starting at buf. For seekable files (regular files, block + * devices), the read begins at the current file offset, and the file of= fset + * is advanced by the number of bytes read. For non-seekable files (pipe= s, + * FIFOs, sockets, character devices), the file offset is not used. + * + * If count is zero and fd refers to a regular file, read() may detect e= rrors + * as described below. In the absence of errors, or if read() does not c= heck + * for errors, a read() with a count of 0 returns zero and has no other = effects. + * + * On success, the number of bytes read is returned (zero indicates end = of + * file for regular files). It is not an error if this number is smaller= than + * the number of bytes requested; this may happen because fewer bytes are + * actually available right now (maybe because we were close to end-of-f= ile, + * or because we are reading from a pipe, socket, or terminal), or becau= se + * read() was interrupted by a signal. + * + * On Linux, read() transfers at most MAX_RW_COUNT (0x7ffff000, approxim= ately + * 2GB) bytes per call, regardless of whether the filesystem would allow= more. + * This is to avoid issues with signed arithmetic overflow on 32-bit sys= tems. + * + * POSIX allows reads that are interrupted after reading some data to ei= ther + * return -1 (with errno set to EINTR) or return the number of bytes alr= eady + * 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 val= ues + * 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 valid, writable user-space memory region of at + * least count bytes. The buffer is validated via access_ok() before a= ny + * read operation. NULL is invalid and will return EFAULT. The buffer = may + * be partially written if an error occurs mid-read. For O_DIRECT read= s, + * the buffer may need to be aligned to the filesystem's block size (v= aries + * by filesystem, check via statx() with STATX_DIOALIGN). + * + * param: count + * type: uint, input + * constraint-type: range(0, SIZE_MAX) + * cdesc: Maximum number of bytes to read. Clamped internally to + * MAX_RW_COUNT (INT_MAX & PAGE_MASK, approximately 0x7ffff000 bytes) = to + * prevent signed overflow issues. A count of 0 returns immediately wi= th 0 + * without accessing the file (but may still detect errors). Large val= ues + * are not errors but will be clamped. Cast to ssize_t must not be neg= ative. + * + * return: + * type: int + * check-type: range + * success: >=3D 0 + * desc: On success, returns the number of bytes read (non-negative). Ze= ro + * 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 rea= ding. + * This includes file descriptors opened with O_WRONLY, O_PATH, or file + * descriptors that have been closed. Also returned if the file struct= ure + * does not have FMODE_READ set. + * + * error: EFAULT, Bad address + * desc: buf points outside the accessible address space. The buffer add= ress + * failed access_ok() validation. Can also occur if a fault happens du= ring + * copy_to_user() when transferring data to user space after the read + * completes in kernel space. + * + * error: EINVAL, Invalid argument + * desc: Returned in several cases: (1) The file descriptor refers to an + * object that is not suitable for reading (no read or read_iter metho= d). + * (2) The file was opened with O_DIRECT and the buffer alignment, off= set, + * or count does not meet the filesystem's alignment requirements. (3)= For + * timerfd file descriptors, the buffer is smaller than 8 bytes. (4) T= he + * count argument, when cast to ssize_t, is negative. + * + * error: EISDIR, Is a directory + * desc: fd refers to a directory. Directories cannot be read using read= (); + * use getdents64() instead. This error is returned by the generic_rea= d_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-b= locking + * (O_NONBLOCK) and the read would block. Also returned with IOCB_NOWA= IT + * when data is not immediately available. Equivalent to EWOULDBLOCK. + * The application should retry the read later or use select/poll/epol= l. + * + * error: EWOULDBLOCK, Operation would block + * desc: Alias of EAGAIN on Linux (identical errno value). POSIX permits + * implementations to distinguish the two; Linux does not. Listed here + * for completeness so tooling that consults the spec does not treat + * EWOULDBLOCK-returning call sites as undocumented. See EAGAIN above + * for the conditions that trigger it. + * + * 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 s= hould + * typically restart the read. + * + * error: EIO, Input/output error + * desc: A low-level I/O error occurred. For regular files, this typical= ly + * indicates a hardware error on the storage device, a filesystem erro= r, + * or a network filesystem timeout. For terminals, this may indicate t= he + * controlling terminal has been closed for a background process. + * + * error: EOVERFLOW, Value too large for defined data type + * desc: The file position plus count would exceed LLONG_MAX. Also retur= ned + * when reading from certain files (e.g., some /proc files) where the = file + * position would overflow. For files without FOP_UNSIGNED_OFFSET flag, + * negative file positions are not allowed. + * + * error: ENOBUFS, No buffer space available + * desc: Returned when reading from pipe-based watch queues (CONFIG_WATC= H_QUEUE) + * when the buffer is too small to hold a complete notification, or wh= en + * 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 si= gnal + * 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 e= ven + * if the file was successfully opened, as LSM policies may enforce pe= r- + * operation checks. + * + * error: EPERM, Operation not permitted + * desc: Returned by fanotify permission events (CONFIG_FANOTIFY_ACCESS_= PERMISSIONS) + * when a user-space fanotify listener denies the read operation via + * fsnotify_file_area_perm(). + * + * error: ENODATA, No data available + * desc: Returned when reading from files backed by fscache/cachefiles + * and the requested data range is not available in the cache + * (e.g., beyond EOF or in an uncached region). Also returned by + * some filesystem-specific read handlers (e.g., xattr reads). + * + * error: EOPNOTSUPP, Operation not supported + * desc: Returned when the file descriptor does not support the read + * operation, such as reading from certain special files or when the + * filesystem does not implement read for this file type. + * + * lock: file->f_pos_lock + * type: mutex + * acquired: conditional + * released: true + * desc: For regular files that require atomic position updates (FMODE_A= TOMIC_POS), + * the f_pos_lock mutex is acquired by fdget_pos() at syscall entry an= d released + * by fdput_pos() at syscall exit. This serializes concurrent reads th= at 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: conditional + * released: true + * desc: The filesystem's read_iter or read method may acquire additiona= l locks. + * For regular files, this typically includes the inode's i_rwsem for = certain + * operations. For pipes, the pipe->mutex is acquired. For sockets, so= cket + * lock is acquired. These are internal to the file operation and rele= ased + * before return. + * + * lock: RCU read-side + * type: rcu + * acquired: conditional + * released: true + * desc: Held transiently during file descriptor table lookup within fdg= et(). + * The RCU read lock is acquired and released internally by the fd loo= kup + * 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 dat= a to + * become available (pipes, sockets, terminals) or waiting for locks. = If + * interrupted before any data is read, returns -EINTR or -ERESTARTSYS. + * If data has already been read, returns the number of bytes read. + * 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 offs= et + * is not used or modified. The offset update is protected by f_pos_lo= ck + * when the file is shared between threads/processes. + * reversible: no + * + * side-effect: modify_state + * target: inode access time (atime) + * condition: When read succeeds and O_NOATIME is not set + * desc: Updates the file's access time (atime) via touch_atime(). The u= pdate + * may be suppressed by mount options (noatime, relatime), the O_NOATI= ME + * flag, or if the filesystem does not support atime. Relatime only up= dates + * atime 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: Always + * desc: Updates the current task's I/O accounting statistics. The rchar= field + * (read characters) is incremented by bytes read via add_rchar() only= on + * successful reads (ret > 0). The syscr field (syscall read count) is + * incremented unconditionally via inc_syscr(). These statistics are v= isible + * 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() all= owing + * inotify, fanotify, and dnotify watchers to be notified of the read.= This + * occurs after data transfer completes successfully. + * reversible: no + * + * capability: CAP_DAC_OVERRIDE + * type: bypass_check + * allows: Bypass discretionary access control on read permission + * without: Standard DAC checks are enforced + * condition: Checked at open time via inode_permission(), not during re= ad() + * + * capability: CAP_DAC_READ_SEARCH + * type: bypass_check + * allows: Bypass read permission checks on regular files + * without: Must have read permission on file + * condition: Checked at open time via inode_permission(), not during re= ad() + * + * constraint: MAX_RW_COUNT + * desc: The count parameter is silently clamped to MAX_RW_COUNT (INT_MA= X & + * PAGE_MASK, approximately 2GB minus one page) to prevent integer ove= rflow + * in internal calculations. This is transparent to the caller; the sy= scall + * succeeds but reads at most MAX_RW_COUNT bytes. + * expr: actual_count =3D 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_RD= WR. + * Files opened with O_WRONLY or O_PATH cannot be read and return EBAD= F. + * The file must have both FMODE_READ and FMODE_CAN_READ flags set. + * expr: (file->f_mode & FMODE_READ) && (file->f_mode & FMODE_CAN_READ) + * + * examples: n =3D read(fd, buf, sizeof(buf)); // Basic read + * n =3D read(STDIN_FILENO, buf, 1024); // Read from stdin + * while ((n =3D read(fd, buf, 4096)) > 0) { process(buf, n); } // Read= loop + * if (read(fd, buf, count) =3D=3D 0) { handle_eof(); } // Check for EOF + * + * notes: The behavior of read() varies significantly depending on the typ= e of + * file descriptor: + * + * - Regular files: Reads from current position, advances position, retu= rns 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 newli= ne is + * entered or buffer is full. Raw mode returns immediately when data a= vailable. + * Special handling for signals (SIGINT on Ctrl+C, etc.). + * + * - Device special files: Behavior is device-specific. Some devices sup= port + * 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 s= hould + * use pread() for concurrent positioned reads. + * + * O_DIRECT reads bypass the page cache and typically require aligned bu= ffers + * and positions. Alignment requirements are filesystem-specific; use st= atx() + * with STATX_DIOALIGN (Linux 6.1+) to query. Unaligned O_DIRECT reads f= ail + * 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); --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1ECCB3C0A12; Fri, 29 May 2026 23:33:40 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097623; cv=none; b=OmHTXAuIduW8ybozmK3pxky5dgCoXzsFbDIMYueuYbttXOuy07foKB89WckSVD9KJi1g38+p9H2eiLan6TWiQ1YvOsIewzBpInge9JTonTTAlVZm9/WOh9UvGVe6AERS5lrt7jW/kNqzWa0we5tSDDKTl4AJdjSc0qjlFtVxzxg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097623; c=relaxed/simple; bh=G2p0eZtY5HuSC3dDtVryrIKl0O5T9UpBRw40+/lIONY=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=B2pl31P//nXCC4ygzcjm3aC+GPKnPmuuH0rAf5kmV1376PEMTTsNy0LifBKTlutoBCufYuW2BwIYlmBKARwXlAjsNp/D0LQil3vf44iJm0S09vp3fWWCRfdZZ3Lw7LKfVdXsKKMbamlkl6oN7Wk5oNCV7AXZCpUEw7mRF/QNu2E= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=QGrgpqpO; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="QGrgpqpO" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 320F51F00893; Fri, 29 May 2026 23:33:37 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097620; bh=lLBUyfcvyVDnlD81AY82kVHbY0P4s4abERaw/Vw9AqI=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=QGrgpqpOfeMpvbQLQxaoXTfSP/gi0AKLkXiqqtkyxWtV7vFYrLg2Lhdei9FXOBhIF QxwuCZfUgjEVlg0Q0p3IBiZGx9aRz6cFXW2vViKjWHhsuw0EPlFLEUhpnYnxtxKtnN bMVlbUVZ9eSSyYDbvp+GAhwXnD0sijsPaDD4cGesCpPehA/hovOUNEq9Kl54RCsF/K 0U4ixEw7he0VzME/2II2WWiFIpsReCLbPEefvjTz7iWbw0mtG9tAEN6uC0jrXZyvsq d4hMlQiFfQmBvdhob5MZybWvOjWXDq5ZsaZDgzFZ9AGDnBgA723qNvdo5QlMr8JQHr sUN6EVguUNRTA== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 08/11] kernel/api: add API specification for sys_write Date: Fri, 29 May 2026 19:33:07 -0400 Message-ID: <20260529233311.1901670-9-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add KAPI-annotated kerneldoc for the sys_write system call in fs/read_write.c. The specification documents parameter constraints (fd, user buffer, count), error conditions, locking requirements, signal handling behavior, and short write semantics. Signed-off-by: Sasha Levin --- fs/read_write.c | 391 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) diff --git a/fs/read_write.c b/fs/read_write.c index 1a5dd11a0f3ed..7c00f8b19ec20 100644 --- a/fs/read_write.c +++ b/fs/read_write.c @@ -1046,6 +1046,397 @@ ssize_t ksys_write(unsigned int fd, const char __us= er *buf, size_t count) return ret; } =20 +/** + * sys_write - Write data to a file descriptor + * @fd: File descriptor to write to + * @buf: User-space buffer containing data to write + * @count: Maximum number of bytes to write + * + * long-desc: Attempts to write up to count bytes from the buffer starting= at + * buf to the file referred to by the file descriptor fd. For seekable f= iles + * (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 th= e file + * was opened with O_APPEND, the file offset is first set to the end of = the + * file before writing. For non-seekable files (pipes, FIFOs, sockets, c= haracter + * devices), the file offset is not used and writing occurs at the curre= nt + * position as defined by the device. + * + * The number of bytes written may be less than count if, for example, t= here is + * insufficient space on the underlying physical medium, or the RLIMIT_F= SIZE + * resource limit is encountered, or the call was interrupted by a signal + * handler after having written less than count bytes. In the event of a + * successful partial write, the caller should make another write() call= to + * transfer the remaining bytes. This behavior is called a "short write." + * + * On Linux, write() transfers at most MAX_RW_COUNT (0x7ffff000, approxi= mately + * 2GB minus one page) bytes per call, regardless of whether the file or + * filesystem would allow more. This prevents signed arithmetic overflow. + * + * For regular files, a successful write() does not guarantee that data = has been + * committed to disk. Use fsync(2) or fdatasync(2) if durability is requ= ired. + * For O_SYNC or O_DSYNC files, the kernel automatically syncs data on w= rite. + * + * POSIX permits writes that are interrupted after partial writes to eit= her + * return -1 with errno=3DEINTR, or to return the count of bytes already= written. + * Linux implements the latter behavior: if some data has been written b= efore + * 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 descri= ptors + * opened with O_RDONLY, O_PATH, or that have been closed return EBADF. + * Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are val= id 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 valid, readable user-space memory region of at + * least count bytes. The buffer is validated via access_ok() before a= ny + * write operation. NULL is invalid and returns EFAULT. For O_DIRECT w= rites, + * the buffer may need to be aligned to the filesystem's block size (v= aries + * by filesystem; query with statx() using STATX_DIOALIGN on Linux 6.1= +). + * + * param: count + * type: uint, input + * constraint-type: range(0, SIZE_MAX) + * cdesc: Maximum number of bytes to write. Clamped internally to + * MAX_RW_COUNT (INT_MAX & PAGE_MASK, approximately 0x7ffff000 bytes) = to + * prevent signed overflow. A count of 0 is passed through to the unde= rlying + * file operation and typically returns 0, but may trigger filesystem + * or driver-specific side effects. Cast to ssize_t must not be negati= ve. + * + * return: + * type: int + * check-type: range + * success: >=3D 0 + * desc: On success, returns the number of bytes written (non-negative).= Zero + * indicates that nothing was written (count was 0, or no space availa= ble + * for non-blocking writes). The return value may be less than count d= ue 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 wri= ting. + * This includes file descriptors opened with O_RDONLY, O_PATH, or file + * descriptors that have been closed. Also returned if the file struct= ure + * does not have FMODE_WRITE set. + * + * error: EFAULT, Bad address + * desc: buf points outside the accessible address space. The buffer add= ress + * failed access_ok() validation. Can also occur if a fault happens du= ring + * copy_from_user() when reading data from user space. + * + * error: EINVAL, Invalid argument + * desc: Returned in several cases: (1) The file descriptor refers to an + * object that is not suitable for writing (no write or write_iter met= hod). + * (2) The file was opened with O_DIRECT and the buffer alignment, off= set, + * or count does not meet the filesystem's alignment requirements. (3)= The + * count argument, when cast to ssize_t, is negative. Also returned if= the + * file lacks the FMODE_CAN_WRITE flag. + * + * error: EAGAIN, Resource temporarily unavailable + * desc: fd refers to a file (pipe, socket, device) that is marked non-b= locking + * (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: EWOULDBLOCK, Operation would block + * desc: Alias of EAGAIN on Linux (identical errno value). POSIX permits + * implementations to distinguish the two; Linux does not. Listed here + * for completeness so tooling that consults the spec does not treat + * EWOULDBLOCK-returning call sites as undocumented. See EAGAIN above + * for the conditions that trigger it. + * + * error: EINTR, Interrupted system call + * desc: The call was interrupted by a signal before any data was writte= n. This + * only occurs if no data has been transferred; if some data was writt= en + * 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 SIG= PIPE + * signal. If the signal is caught or ignored, EPIPE is still returned. + * For sockets, MSG_NOSIGNAL (via send()) suppresses the signal. For + * pwritev2(), the RWF_NOSIGNAL flag suppresses it. + * + * error: EFBIG, File too large + * desc: An attempt was made to write a file that exceeds the implementa= tion- + * defined maximum file size or the file size limit (RLIMIT_FSIZE) of = the + * process. When RLIMIT_FSIZE is exceeded, the process also receives S= IGXFSZ. + * For files not opened with O_LARGEFILE on 32-bit systems, the limit = is 2GB. + * + * error: ENOSPC, No space left on device + * desc: The device containing the file has no room for the data. This c= an + * occur mid-write resulting in a short write followed by ENOSPC on re= try. + * + * error: EDQUOT, Disk quota exceeded + * desc: The user's quota of disk blocks on the filesystem has been exha= usted. + * 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 wri= ting + * data. This typically indicates hardware failure, filesystem corrupt= ion, + * or network filesystem timeout. Some data may have been written. + * + * error: EPERM, Operation not permitted + * desc: The operation was prevented: (1) by a file seal (F_SEAL_WRITE or + * F_SEAL_FUTURE_WRITE on memfd/shmem), (2) writing to an immutable in= ode + * (IS_IMMUTABLE), (3) by an LSM hook denying the operation, or (4) by= a + * fanotify permission event denying the write. + * + * error: EOVERFLOW, Value too large for defined data type + * desc: The file position plus count would exceed LLONG_MAX. Also retur= ned + * when the offset would exceed filesystem limits after the write. + * + * error: EDESTADDRREQ, Destination address required + * desc: fd is a datagram socket for which no peer address has been set = using + * connect(2). Use sendto(2) to specify the destination address. + * + * error: ETXTBSY, Text file busy + * desc: The file is being used as a swap file (IS_SWAPFILE). Note: unli= ke + * the traditional Unix meaning, Linux does not return ETXTBSY when wr= iting + * to an executing binary; that only blocks open() with O_WRONLY/O_RDW= R. + * + * 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 operatio= n. + * 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 handle= r, or + * the syscall is transparently restarted if SA_RESTART is set. User s= pace + * 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: conditional + * released: true + * desc: For regular files that require atomic position updates (FMODE_A= TOMIC_POS), + * the f_pos_lock mutex is acquired by fdget_pos() at syscall entry an= d released + * by fdput_pos() at syscall exit. This serializes concurrent writes s= haring + * the same file description. Not acquired for stream files (FMODE_STR= EAM like + * pipes and sockets) or when the file is not shared. + * + * lock: sb->s_writers (freeze protection) + * type: custom + * acquired: conditional + * released: true + * desc: For regular files, file_start_write() acquires freeze protectio= n on + * the superblock via sb_start_write() before the write, and file_end_= write() + * releases it after. This prevents writes during filesystem freeze. N= ot + * acquired for non-regular files (pipes, sockets, devices). + * + * lock: inode->i_rwsem + * type: rwlock + * acquired: conditional + * released: true + * desc: For regular files using generic_file_write_iter(), the inode's = i_rwsem + * is acquired in write mode before modifying file data. This is inter= nal to + * the filesystem and released before return. Not all filesystems use = this + * pattern. + * + * lock: pipe->mutex + * type: mutex + * acquired: conditional + * released: true + * desc: For pipes and FIFOs, the pipe's mutex is held while modifying p= ipe + * buffers. Released temporarily while waiting for space, then reacqui= red. + * + * lock: RCU read-side + * type: rcu + * acquired: conditional + * released: true + * desc: Held transiently during file descriptor table lookup within fdg= et(). + * The RCU read lock is acquired and released internally by the fd loo= kup + * path, not held across the entire syscall. fdput() releases the file + * reference count, not the RCU lock. + * + * signal: 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 wh= ose + * peer has closed, SIGPIPE is sent to the calling process. The default + * action terminates the process. Use signal(SIGPIPE, SIG_IGN) to supp= ress + * for write(). EPIPE is returned regardless of signal disposition. + * timing: during + * + * signal: SIGXFSZ + * direction: send + * action: coredump + * condition: Writing exceeds RLIMIT_FSIZE + * desc: When a write would exceed the soft file size limit (RLIMIT_FSIZ= E), + * SIGXFSZ is sent. The default action terminates with a core dump. The + * write returns EFBIG. If RLIMIT_FSIZE is RLIM_INFINITY, no signal is= sent. + * timing: 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 buf= fer + * space to become available. If interrupted before any data is writte= n, + * returns -EINTR or -ERESTARTSYS. If data was already written, return= s the + * byte count. Restartable if SA_RESTART is set and no data was writte= n. + * 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 writ= ten. + * 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 i= s 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: When write succeeds (returns > 0) + * desc: Updates the file's modification time (mtime) and change time (c= time) + * via file_update_time(). The update precision depends on filesystem = mount + * options (fine-grained timestamps for multigrain inodes). + * reversible: no + * + * side-effect: modify_state + * target: SUID/SGID bits (mode) + * condition: When writing to a setuid/setgid file + * desc: The SUID bit is cleared when a non-root user writes to a file w= ith + * the bit set. The SGID bit may also be cleared. This is a security f= eature + * to prevent privilege escalation via modified setuid binaries. Done = via + * file_remove_privs(). + * 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 wr= itten + * to the page cache (buffered I/O) or directly to storage (O_DIRECT). + * Data may not be persistent until fsync() is called or the file is c= losed. + * reversible: no + * + * side-effect: modify_state + * target: task I/O accounting + * condition: Always + * desc: Updates the current task's I/O accounting statistics. The wchar= field + * (write characters) is incremented by bytes written via add_wchar() = only on + * successful writes (ret > 0). The syscw field (syscall write count) = is + * incremented via inc_syscw() when the write operation is attempted + * (after passing initial validation checks). These statistics are vis= ible + * 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(), al= lowing + * inotify, fanotify, and dnotify watchers to be notified of the write. + * + * capability: CAP_DAC_OVERRIDE + * type: bypass_check + * allows: Bypass discretionary access control on write permission + * without: Standard DAC checks are enforced + * condition: Checked at open time via inode_permission(), not during re= ad() + * + * capability: CAP_FSETID + * type: bypass_check + * allows: Bypass ownership checks for SUID/SGID clearing + * without: SUID/SGID bits are cleared on write by non-owner + * condition: Checked during file_remove_privs() + * + * constraint: MAX_RW_COUNT + * desc: The count parameter is silently clamped to MAX_RW_COUNT (INT_MA= X & + * PAGE_MASK, approximately 2GB minus one page) to prevent integer ove= rflow + * in internal calculations. This is transparent to the caller. + * expr: actual_count =3D 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_RD= WR. + * Files opened with O_RDONLY or O_PATH cannot be written and return E= BADF. + * 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 res= ource + * limit. If writing would exceed this limit, SIGXFSZ is sent and EFBI= G is + * returned. The limit does not apply to files beyond the limit - only= to + * writes that would cross it. + * expr: pos + count <=3D rlimit(RLIMIT_FSIZE) || rlimit(RLIMIT_FSIZE) = =3D=3D RLIM_INFINITY + * + * constraint: File seals + * desc: For memfd or shmem files with F_SEAL_WRITE or F_SEAL_FUTURE_WRI= TE + * seals applied, all write operations fail with EPERM. With F_SEAL_GR= OW, + * writes that would extend file size fail with EPERM. + * + * examples: n =3D write(fd, buf, sizeof(buf)); // Basic write + * n =3D write(STDOUT_FILENO, msg, strlen(msg)); // Write to stdout + * // Handle short writes: + * while (total < len) { + * n =3D write(fd, buf + total, len - total); + * if (n < 0) break; + * total +=3D n; + * } + * // Pipe error handling: + * if (write(pipefd[1], &byte, 1) < 0 && errno =3D=3D EPIPE) + * handle_broken_pipe(); + * + * notes: The behavior of write() varies significantly depending on the ty= pe of + * file descriptor: + * + * - Regular files: Writes to the page cache (buffered) or directly to s= torage + * (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 b= ytes + * on Linux) are guaranteed atomic. Larger writes may be interleaved w= ith + * 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 socke= ts + * (TCP) may return partial writes. Datagram sockets (UDP) typically w= rite + * complete messages or fail. SIGPIPE/EPIPE for broken connections (un= less + * 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 be= have + * 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 atom= ic + * 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 sta= tx() + * with STATX_DIOALIGN (Linux 6.1+). Unaligned O_DIRECT writes return EI= NVAL + * on most filesystems. + * + * For zero-copy writes, consider using splice(2), sendfile(2), or vmspl= ice(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) { --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 31FCC3CC7F8; Fri, 29 May 2026 23:33:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097626; cv=none; b=M3NC7vecB7QJV5TSAsPmDfLyPJiqTC58ixeOH9ziJC2GB1+2CEiYGomZ3kErBVtrCAA1946Ainz3GL8KAXsUkJcKT6BJMEBTMns9aSSX7GN9/Y4DIgQm3C+MQbhvQG/Usoi45RPgAKlrt+9XbEQWIm9sWZJD9etnKNuHRfWYrbQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097626; c=relaxed/simple; bh=lO2tIetRubgvaN/PQ1r8A/l5tDuosV6OMtUW+eArBEU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=l0/8H4u9F/moXfoderyqSk4eRJG2LaADyBIjGMWOaCtr1VoS81ZjpSwlx2EeRb4B/4jgMGzw6Ri0ogZOisl0UiYFD7upON9yY2e+pwJsZpkiCi0GUPBBJaSbYIpOlI6OFDpgTSobO4aUGUewrDLcFA5DdZf/HtMl+c3UzCCgDVs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=aCplmvzg; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="aCplmvzg" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 479DB1F00898; Fri, 29 May 2026 23:33:40 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097623; bh=EvtCP/NBwpAOK5Tqyn5HqiR/6h1vYH3aclWiK5NNn6E=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=aCplmvzgEe1KxKwuA0ZmVzjbeCkWdfOYRrod4NZutcvXUYKmdDq7GL+IfwGVuVAFs Pl+UGi1PnAPNJ2ccH/I9cE9CtyLGeB+iSL1VEdPjsxcoPrIALiP7R+OY1EByWQXXYp K+AwQMk9fOhktTeCTVRrht48iNZoxoPB9EPFSeqAfONpgUCOYgH/WzB/uxccF5daNu cAa7uMRy0rX7s6hR03gDWXMhsDQkk59XSd4/sNXJFiniq9Tq3HHpW2bqkHis0IUdGZ 5yCj6CvXwoA3k9mcQrFKBgxWbunCdXKVnjtm0PeIRybGYuxzZfcTsP46iZYhMnLq2H FJc97k41pivVA== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 09/11] kernel/api: add runtime verification selftest Date: Fri, 29 May 2026 19:33:08 -0400 Message-ID: <20260529233311.1901670-10-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add a selftest for CONFIG_KAPI_RUNTIME_CHECKS that exercises sys_open/sys_read/sys_write/sys_close through raw syscall() and verifies KAPI pre-validation catches invalid parameters while allowing valid operations through. Test cases (TAP output): 1-4: Valid open/read/write/close succeed 5-7: Invalid flags, mode bits, NULL path rejected with EINVAL 8: dmesg contains expected KAPI warning strings Signed-off-by: Sasha Levin --- MAINTAINERS | 1 + tools/testing/selftests/Makefile | 1 + tools/testing/selftests/kapi/Makefile | 7 + tools/testing/selftests/kapi/kapi_test_util.h | 33 + tools/testing/selftests/kapi/test_kapi.c | 1096 +++++++++++++++++ 5 files changed, 1138 insertions(+) create mode 100644 tools/testing/selftests/kapi/Makefile create mode 100644 tools/testing/selftests/kapi/kapi_test_util.h create mode 100644 tools/testing/selftests/kapi/test_kapi.c diff --git a/MAINTAINERS b/MAINTAINERS index 0d14205077908..ddfd9cad98916 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13826,6 +13826,7 @@ F: include/linux/kernel_api_spec.h F: kernel/api/ F: tools/kapi/ F: tools/lib/python/kdoc/kdoc_apispec.py +F: tools/testing/selftests/kapi/ =20 KERNEL AUTOMOUNTER M: Ian Kent diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Mak= efile index 450f13ba4cca9..7881bec5aafe1 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -48,6 +48,7 @@ TARGETS +=3D intel_pstate TARGETS +=3D iommu TARGETS +=3D ipc TARGETS +=3D ir +TARGETS +=3D kapi TARGETS +=3D kcmp TARGETS +=3D kexec TARGETS +=3D kselftest_harness diff --git a/tools/testing/selftests/kapi/Makefile b/tools/testing/selftest= s/kapi/Makefile new file mode 100644 index 0000000000000..32a750901b111 --- /dev/null +++ b/tools/testing/selftests/kapi/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 + +TEST_GEN_PROGS :=3D test_kapi + +CFLAGS +=3D -static -Wall -Wextra -Werror -O2 $(KHDR_INCLUDES) + +include ../lib.mk diff --git a/tools/testing/selftests/kapi/kapi_test_util.h b/tools/testing/= selftests/kapi/kapi_test_util.h new file mode 100644 index 0000000000000..e097c370542ad --- /dev/null +++ b/tools/testing/selftests/kapi/kapi_test_util.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2026 Sasha Levin + * + * Compatibility helpers for KAPI selftests. + * + * __NR_open is not defined on aarch64 and riscv64 (only __NR_openat exist= s). + * Provide a wrapper that uses __NR_openat with AT_FDCWD to achieve the sa= me + * behavior as __NR_open on architectures that lack it. + */ +#ifndef KAPI_TEST_UTIL_H +#define KAPI_TEST_UTIL_H + +#include +#include + +#ifndef __NR_open +/* + * On architectures without __NR_open (e.g., aarch64, riscv64), + * use openat(AT_FDCWD, ...) which is equivalent. + */ +static inline long kapi_sys_open(const char *pathname, int flags, int mode) +{ + return syscall(__NR_openat, AT_FDCWD, pathname, flags, mode); +} +#else +static inline long kapi_sys_open(const char *pathname, int flags, int mode) +{ + return syscall(__NR_open, pathname, flags, mode); +} +#endif + +#endif /* KAPI_TEST_UTIL_H */ diff --git a/tools/testing/selftests/kapi/test_kapi.c b/tools/testing/selft= ests/kapi/test_kapi.c new file mode 100644 index 0000000000000..a6b7576f95c3e --- /dev/null +++ b/tools/testing/selftests/kapi/test_kapi.c @@ -0,0 +1,1096 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026 Sasha Levin + * + * 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=3Dy for full coverage; many tests + * also pass without it. + * + * TAP output format. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../kselftest.h" +#include "kapi_test_util.h" + +#define NUM_TESTS 29 + +/* + * Set from the SIGPIPE handler. `volatile sig_atomic_t` is the POSIX- + * mandated type for flags touched by async-signal-safe handlers; + * checkpatch's generic "volatile considered harmful" warning targets + * kernel code and does not apply here. + */ +static volatile sig_atomic_t got_sigpipe; + +/* + * The tap_* helpers are thin wrappers around ksft_test_result_* so the + * rest of this file reads like the original author wrote it, while the + * output goes through the shared kselftest harness. + */ +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 the kernel provides the kapi runtime-check surface. + * Tests that rely on KAPI rejecting bad parameters pre-call should be + * skipped on kernels without it, not reported as failures. + */ +static bool kapi_runtime_checks_active(void) +{ + struct stat st; + + return stat("/sys/kernel/debug/kapi", &st) =3D=3D 0 && S_ISDIR(st.st_mode= ); +} + +static void sigpipe_handler(int sig) +{ + (void)sig; + got_sigpipe =3D 1; +} + +/* ---- Valid operation tests ---- */ + +/* + * Test 1: open a readable file + * Returns fd on success. + */ +static int test_open_valid(void) +{ + errno =3D 0; + long fd =3D kapi_sys_open("/etc/hostname", O_RDONLY, 0); + + if (fd >=3D 0) { + tap_ok("open valid file"); + } else { + /* /etc/hostname might not exist; try /etc/passwd */ + errno =3D 0; + fd =3D kapi_sys_open("/etc/passwd", O_RDONLY, 0); + if (fd >=3D 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 =3D 0; + long ret =3D syscall(__NR_read, fd, buf, sizeof(buf)); + + if (ret > 0) + tap_ok("read from valid fd"); + else if (ret =3D=3D 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 =3D 0; + long devnull =3D kapi_sys_open("/dev/null", O_WRONLY, 0); + + if (devnull < 0) { + tap_fail("write to /dev/null (open failed)", strerror(errno)); + return; + } + + errno =3D 0; + long ret =3D syscall(__NR_write, (int)devnull, "hello", 5); + + if (ret =3D=3D 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 =3D 0; + long ret =3D syscall(__NR_close, fd); + + if (ret =3D=3D 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 =3D 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 =3D kapi_sys_open("/dev/null", 0x10000000, 0); + + if (ret =3D=3D -1 && errno =3D=3D EINVAL) { + tap_ok("open with invalid flags returns EINVAL"); + } else if (ret >=3D 0) { + tap_fail("open with invalid flags", "expected EINVAL, got success"); + syscall(__NR_close, (int)ret); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected EINVAL, got %s", + strerror(errno)); + tap_fail("open with invalid flags", msg); + } +} + +/* + * Test 6: open with invalid mode bits + * 0xFFFF has bits outside S_IALLUGO (07777), KAPI should reject. + */ +static void test_open_invalid_mode(void) +{ + long ret; + + if (!kapi_runtime_checks_active()) { + tap_skip("open with invalid mode", + "CONFIG_KAPI_RUNTIME_CHECKS not enabled"); + return; + } + + errno =3D 0; + ret =3D kapi_sys_open("/tmp/kapi_test_mode", + O_CREAT | O_WRONLY | O_EXCL, 0xFFFF); + + if (ret =3D=3D -1 && errno =3D=3D EINVAL) { + tap_ok("open with invalid mode returns EINVAL"); + } else if (ret >=3D 0) { + tap_fail("open with invalid mode", "expected EINVAL, got success"); + syscall(__NR_close, (int)ret); + unlink("/tmp/kapi_test_mode"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected EINVAL, got %s", + strerror(errno)); + tap_fail("open with invalid mode", msg); + } +} + +/* + * Test 7: open with NULL path + * KAPI USER_PATH constraint should reject NULL. + */ +static void test_open_null_path(void) +{ + errno =3D 0; + long ret =3D kapi_sys_open(NULL, O_RDONLY, 0); + + if (ret =3D=3D -1 && errno =3D=3D EINVAL) { + tap_ok("open with NULL path returns EINVAL"); + } else if (ret =3D=3D -1 && errno =3D=3D EFAULT) { + /* Kernel may catch this as EFAULT before KAPI */ + tap_ok("open with NULL path returns EFAULT (acceptable)"); + } else if (ret >=3D 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 =3D 0; + ret =3D kapi_sys_open("/dev/null", 0x40000000, 0); + + if (ret =3D=3D -1 && errno =3D=3D EINVAL) { + tap_ok("open with flag bit 30 (0x40000000) returns EINVAL"); + } else if (ret >=3D 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=3D-1 should return an error. + * With CONFIG_KAPI_RUNTIME_CHECKS=3Dy, 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 =3D 0; + long ret =3D syscall(__NR_read, -1, buf, sizeof(buf)); + + if (ret =3D=3D -1 && (errno =3D=3D EBADF || errno =3D=3D EINVAL)) { + tap_ok("read with fd=3D-1 returns error"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected EBADF/EINVAL, got %s", + ret >=3D 0 ? "success" : strerror(errno)); + tap_fail("read with fd=3D-1 returns error", msg); + } +} + +/* + * Test 10: read with count=3D0 should return 0 + */ +static void test_read_zero_count(void) +{ + char buf[1]; + long fd; + + errno =3D 0; + fd =3D kapi_sys_open("/dev/null", O_RDONLY, 0); + if (fd < 0) { + tap_fail("read with count=3D0 returns 0", + "cannot open /dev/null"); + return; + } + + errno =3D 0; + long ret =3D syscall(__NR_read, (int)fd, buf, 0); + + if (ret =3D=3D 0) { + tap_ok("read with count=3D0 returns 0"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=3D%s)", + ret, strerror(errno)); + tap_fail("read with count=3D0 returns 0", msg); + } + + syscall(__NR_close, (int)fd); +} + +/* + * Test 11: write with count=3D0 should return 0 + */ +static void test_write_zero_count(void) +{ + long fd; + + errno =3D 0; + fd =3D kapi_sys_open("/dev/null", O_WRONLY, 0); + if (fd < 0) { + tap_fail("write with count=3D0 returns 0", + "cannot open /dev/null"); + return; + } + + errno =3D 0; + long ret =3D syscall(__NR_write, (int)fd, "x", 0); + + if (ret =3D=3D 0) { + tap_ok("write with count=3D0 returns 0"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=3D%s)", + ret, strerror(errno)); + tap_fail("write with count=3D0 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 =3D PATH_MAX + 256; + + longpath =3D malloc(len); + if (!longpath) { + tap_fail("open with path > PATH_MAX", "malloc failed"); + return; + } + + memset(longpath, 'A', len - 1); + longpath[0] =3D '/'; + longpath[len - 1] =3D '\0'; + + errno =3D 0; + long ret =3D kapi_sys_open(longpath, O_RDONLY, 0); + + if (ret =3D=3D -1 && (errno =3D=3D ENAMETOOLONG || errno =3D=3D EINVAL)) { + tap_ok("open with path > PATH_MAX returns ENAMETOOLONG/EINVAL"); + } else if (ret >=3D 0) { + tap_fail("open with path > PATH_MAX", + "expected error, got success"); + syscall(__NR_close, (int)ret); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), + "expected ENAMETOOLONG/EINVAL, got %s", + strerror(errno)); + tap_fail("open with path > PATH_MAX", msg); + } + + free(longpath); +} + +/* + * Test 13: read with unmapped user pointer should return EFAULT or EINVAL. + * Use a pipe with data so the kernel actually tries to copy to the buffer. + */ +static void test_read_unmapped_buf(void) +{ + int pipefd[2]; + + if (pipe(pipefd) < 0) { + tap_fail("read with unmapped buffer returns EFAULT/EINVAL", + "pipe() failed"); + return; + } + + /* Write some data so read has something to copy */ + (void)write(pipefd[1], "hello", 5); + + errno =3D 0; + long ret =3D syscall(__NR_read, pipefd[0], (void *)0xDEAD0000, 16); + + if (ret =3D=3D -1 && (errno =3D=3D EFAULT || errno =3D=3D EINVAL)) { + tap_ok("read with unmapped buffer returns EFAULT/EINVAL"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), + "expected EFAULT/EINVAL, got %s", + ret >=3D 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 EINVA= L. + * 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 =3D 0; + long ret =3D syscall(__NR_write, pipefd[1], (void *)0xDEAD0000, 16); + + if (ret =3D=3D -1 && (errno =3D=3D EFAULT || errno =3D=3D EINVAL)) { + tap_ok("write with unmapped buffer returns EFAULT/EINVAL"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), + "expected EFAULT/EINVAL, got %s", + ret >=3D 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 =3D 0; + fd =3D 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 =3D 0; + long ret =3D syscall(__NR_close, (int)fd); + + if (ret =3D=3D -1 && errno =3D=3D EBADF) { + tap_ok("close already-closed fd returns EBADF"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected EBADF, got %s", + ret =3D=3D 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 =3D 0; + long fd =3D kapi_sys_open("/dev/null", O_RDONLY | O_CLOEXEC, 0); + + if (fd >=3D 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 =3D 0; + fd =3D 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 =3D 0; + long ret =3D syscall(__NR_write, (int)fd, "", 0); + + if (ret =3D=3D 0) { + tap_ok("write 0 bytes to /dev/null returns 0"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=3D%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 =3D 0; + fd =3D 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 =3D 0; + long ret =3D syscall(__NR_read, (int)fd, buf, sizeof(buf)); + + if (ret =3D=3D -1 && errno =3D=3D EBADF) { + tap_ok("read from write-only fd returns EBADF"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected EBADF, got %s", + ret >=3D 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 =3D 0; + fd =3D 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 =3D 0; + long ret =3D syscall(__NR_write, (int)fd, "hello", 5); + + if (ret =3D=3D -1 && errno =3D=3D EBADF) { + tap_ok("write to read-only fd returns EBADF"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected EBADF, got %s", + ret >=3D 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 =3D 0; + long ret =3D syscall(__NR_close, 9999); + + if (ret =3D=3D -1 && errno =3D=3D EBADF) { + tap_ok("close fd 9999 returns EBADF"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected EBADF, got %s", + ret =3D=3D 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 =3D 0; + long ret =3D syscall(__NR_read, pipefd[0], buf, sizeof(buf)); + + if (ret =3D=3D 0) { + tap_ok("read from closed pipe returns 0 (EOF)"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=3D%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 =3D sigpipe_handler; + sigemptyset(&sa.sa_mask); + sigaction(SIGPIPE, &sa, &old_sa); + + got_sigpipe =3D 0; + + /* Close read end */ + close(pipefd[0]); + + errno =3D 0; + long ret =3D syscall(__NR_write, pipefd[1], "hello", 5); + + if (ret =3D=3D -1 && errno =3D=3D EPIPE && got_sigpipe) { + tap_ok("write to closed pipe returns EPIPE + SIGPIPE"); + } else if (ret =3D=3D -1 && errno =3D=3D 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=3D%d)", + ret >=3D 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 regular file returns ENOTDIR + */ +static void test_open_directory_on_file(void) +{ + errno =3D 0; + long ret =3D kapi_sys_open("/dev/null", O_RDONLY | O_DIRECTORY, 0); + + if (ret =3D=3D -1 && errno =3D=3D ENOTDIR) { + tap_ok("open O_DIRECTORY on regular file returns ENOTDIR"); + } else if (ret >=3D 0) { + tap_fail("open O_DIRECTORY on regular file", + "expected ENOTDIR, got success"); + syscall(__NR_close, (int)ret); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected ENOTDIR, got %s", + strerror(errno)); + tap_fail("open O_DIRECTORY on regular file", msg); + } +} + +/* + * Test 24: open nonexistent file without O_CREAT returns ENOENT + */ +static void test_open_nonexistent(void) +{ + errno =3D 0; + long ret =3D kapi_sys_open("/tmp/kapi_nonexistent_file_12345", + O_RDONLY, 0); + + if (ret =3D=3D -1 && errno =3D=3D ENOENT) { + tap_ok("open nonexistent file without O_CREAT returns ENOENT"); + } else if (ret >=3D 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 =3D dup(0); + + if (saved_stdin < 0) { + tap_fail("close stdin succeeds", "cannot dup stdin"); + return; + } + + errno =3D 0; + long ret =3D syscall(__NR_close, 0); + + if (ret =3D=3D 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 =3D 0; + fd =3D 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 =3D 0; + long ret =3D syscall(__NR_read, (int)fd, buf, sizeof(buf)); + + if (ret =3D=3D -1 && errno =3D=3D EBADF) { + tap_ok("read after close returns EBADF"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected EBADF, got %s", + ret >=3D 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] =3D "test data"; + + errno =3D 0; + fd =3D 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 =3D 0; + long ret =3D 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 =3D=3D -1 && (errno =3D=3D EFAULT || errno =3D=3D EINVAL))= { + tap_ok("write with large count returns EFAULT/EINVAL (KAPI validates buf= fer)"); + } else { + char msg[64]; + + snprintf(msg, sizeof(msg), "expected success or EFAULT, got %s", + ret =3D=3D 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 =3D 1; + char reason[128] =3D ""; + + /* Open a readable file */ + errno =3D 0; + rd_fd =3D kapi_sys_open("/etc/hostname", O_RDONLY, 0); + if (rd_fd < 0) { + errno =3D 0; + rd_fd =3D kapi_sys_open("/etc/passwd", O_RDONLY, 0); + } + if (rd_fd < 0) { + snprintf(reason, sizeof(reason), "open readable file: %s", + strerror(errno)); + ok =3D 0; + } + + /* Read from it */ + if (ok) { + errno =3D 0; + long n =3D syscall(__NR_read, (int)rd_fd, buf, sizeof(buf)); + + if (n < 0) { + snprintf(reason, sizeof(reason), "read: %s", + strerror(errno)); + ok =3D 0; + } + } + + /* Open /dev/null for writing */ + wr_fd =3D -1; + if (ok) { + errno =3D 0; + wr_fd =3D kapi_sys_open("/dev/null", O_WRONLY, 0); + if (wr_fd < 0) { + snprintf(reason, sizeof(reason), + "open /dev/null: %s", strerror(errno)); + ok =3D 0; + } + } + + /* Write to /dev/null */ + if (ok) { + errno =3D 0; + long n =3D syscall(__NR_write, (int)wr_fd, "test", 4); + + if (n !=3D 4) { + snprintf(reason, sizeof(reason), "write: %s", + n < 0 ? strerror(errno) : "short write"); + ok =3D 0; + } + } + + /* Close both fds */ + if (rd_fd >=3D 0) { + errno =3D 0; + if (syscall(__NR_close, (int)rd_fd) !=3D 0 && ok) { + snprintf(reason, sizeof(reason), "close read fd: %s", + strerror(errno)); + ok =3D 0; + } + } + + if (wr_fd >=3D 0) { + errno =3D 0; + if (syscall(__NR_close, (int)wr_fd) !=3D 0 && ok) { + snprintf(reason, sizeof(reason), "close write fd: %s", + strerror(errno)); + ok =3D 0; + } + } + + if (ok) + tap_ok("normal syscall path (open/read/write/close) works"); + else + tap_fail("normal syscall path (open/read/write/close) works", + reason); +} + +/* + * Test 29: verify dmesg contains KAPI warnings for the invalid tests + */ +static void test_dmesg_warnings(void) +{ + int kmsg_fd =3D open("/dev/kmsg", O_RDONLY | O_NONBLOCK); + + if (kmsg_fd < 0) { + tap_skip("dmesg contains expected KAPI warnings", + "cannot open /dev/kmsg"); + return; + } + + /* + * Rewind to the start of kmsg. SEEK_DATA on /dev/kmsg is the + * documented way to skip to the first entry still in the ring + * buffer. Older kernels (or CONFIG_PRINTK=3Dn builds) may reject + * the seek with -EINVAL; in that case we can't reliably audit + * past warnings, so skip the test rather than fail it. + */ + if (lseek(kmsg_fd, 0, SEEK_DATA) =3D=3D (off_t)-1) { + tap_skip("dmesg contains expected KAPI warnings", + "lseek(SEEK_DATA) not supported on /dev/kmsg"); + close(kmsg_fd); + return; + } + + char line[4096]; + int found_invalid_bits =3D 0; + int found_null =3D 0; + ssize_t n; + + for (;;) { + n =3D read(kmsg_fd, line, sizeof(line) - 1); + if (n > 0) { + line[n] =3D '\0'; + if (strstr(line, "contains invalid bits")) + found_invalid_bits++; + if (strstr(line, "NULL") && strstr(line, "not allowed")) + found_null++; + } else if (n =3D=3D -1 && errno =3D=3D EPIPE) { + /* Ring buffer wrapped, continue reading */ + continue; + } else { + /* EAGAIN (no more messages) or other error */ + break; + } + } + + close(kmsg_fd); + + if (found_invalid_bits >=3D 2 && found_null >=3D 1) { + tap_ok("dmesg contains expected KAPI warnings"); + } else if (found_invalid_bits >=3D 1 || found_null >=3D 1) { + char msg[128]; + + snprintf(msg, sizeof(msg), + "partial: invalid_bits=3D%d null=3D%d", + found_invalid_bits, found_null); + tap_ok(msg); + } else { + tap_fail("dmesg KAPI warnings", + "no KAPI warnings found in dmesg"); + } +} + +int main(void) +{ + ksft_print_header(); + ksft_set_plan(NUM_TESTS); + + /* Valid operations (1-4) */ + int fd =3D test_open_valid(); + + if (fd >=3D 0) + test_read_valid(fd); + else + tap_fail("read from valid fd", "no fd from open"); + + test_write_valid(); + + if (fd >=3D 0) + test_close_valid(fd); + else + tap_fail("close valid fd", "no fd from open"); + + /* KAPI parameter rejection (5-8) */ + test_open_invalid_flags(); + test_open_invalid_mode(); + test_open_null_path(); + test_open_flag_bit30(); + + /* Boundary conditions and error paths (9-20) */ + test_read_bad_fd(); + test_read_zero_count(); + test_write_zero_count(); + test_open_long_path(); + test_read_unmapped_buf(); + test_write_unmapped_buf(); + test_close_already_closed(); + test_open_valid_cloexec(); + test_write_zero_devnull(); + test_read_writeonly_fd(); + test_write_readonly_fd(); + test_close_fd_9999(); + + /* Pipe and lifecycle tests (21-27) */ + test_read_closed_pipe(); + test_write_closed_pipe(); + test_open_directory_on_file(); + test_open_nonexistent(); + test_close_stdin(); + test_read_after_close(); + test_write_large_count(); + + /* Integration (28-29) */ + test_normal_path(); + test_dmesg_warnings(); + + ksft_finished(); + return 0; +} --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3FF4B3CDBAA; Fri, 29 May 2026 23:33:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097629; cv=none; b=Nf1iCbQOf3YtWr1C/+8h2thXRA8cmPsxKfpp2q9AM/I89qWqP8vIYLSx8Y3eN8URwt6GW0rixgVJUIPz27Dv5v1yZp5VXxd9WkG4HMQA+eicIsq7zmoSqECq7tR9U1YMv+HWKuFfhf4W1kR7RIuIkCO2FRB5J8vqZ8dvWJmd2CQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097629; c=relaxed/simple; bh=b6+6c/VZ+E172jIwDrd2mUv5OAZvS4yOQTpwifWIa8A=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=m/5TMDEJWPBJhjFW5a9DwL6YwWJ8U9YOGcfUTR4vt5pBmNVhMnidC+JCZ7g7Le248rrJwoJNgEc7uIy8llQGd3VSe2zgMJRhcW/B3HgdZpWGgiNd2upudkiTEWVSSimghe/Wwhnzq6VkAmnmbsJt5LMCjCdfGNor/nCUaRM4mtU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=M2zelxXZ; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="M2zelxXZ" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 594651F0089B; Fri, 29 May 2026 23:33:43 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097626; bh=rxZR8fNl/BxdvW6Xsew/4k8fTGgO3AxnPtUENX1h1jo=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=M2zelxXZHI67vwKv6UETdHZDGejPH0oqyUtJaj3LyESQ+HOOA+FABV14j/911YNdA gzxrdO8wJc8OXq36VbPrd7q0/HGnwE4j3JUBQA2UTugdwmEnkqQqaW4/s7emzeNCN3 4DrnSmll0I6MoyAnDPax0+yXPoUkojYfERl9JNAW7Q9QddttDynC4ASES9lUQqB7s2 5X975fJGYGC5wN5VHcjqZsKzDG+71NElLQA+u7eRstIRWoF2HsBgeE2kvZP2uT6JBE su4T48pPP75oHOtdYg/4jYRLpx0G0O7lKSO9w1x/MbdoOsdhkDJahf9x6NgQBRR6Hh LtCJKbgAbpzew== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 10/11] kernel/api: add API specification for sys_madvise Date: Fri, 29 May 2026 19:33:09 -0400 Message-ID: <20260529233311.1901670-11-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add KAPI-annotated kerneldoc for the sys_madvise system call in mm/madvise.c. The specification documents parameter constraints (start, len_in, behavior), per-behavior error conditions, lock acquisition (mmap_lock read and write modes plus the per-VMA fast path, mmu_gather and mmu_notifier brackets), signal handling, side effects, capability requirements (CAP_SYS_ADMIN for MADV_HWPOISON and MADV_SOFT_OFFLINE), mseal interaction, and the heterogeneous skip semantics across the hint, immediate-action and destructive groups. Signed-off-by: Sasha Levin --- mm/madvise.c | 575 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 575 insertions(+) diff --git a/mm/madvise.c b/mm/madvise.c index dbb69400786d1..ed0a046e9e25b 100644 --- a/mm/madvise.c +++ b/mm/madvise.c @@ -2032,6 +2032,581 @@ int do_madvise(struct mm_struct *mm, unsigned long = start, size_t len_in, int beh return error; } =20 +/** + * 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 ra= nge + * + * long-desc: Provides the kernel with advice or directions about the addr= ess + * 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 + * . The semantics fall into three groups. The hint group + * primarily updates VMA flags (MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIA= L, + * MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP, MADV_WIPEONFO= RK, + * MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE, MADV_HUGEPAGE, + * MADV_NOHUGEPAGE), and is itself heterogeneous: the fork-copy gates + * (MADV_DONTFORK / MADV_DOFORK) and the KSM scan gates + * (MADV_MERGEABLE / MADV_UNMERGEABLE) are strictly honored by their + * consumers; MADV_HUGEPAGE / MADV_NOHUGEPAGE express THP eligibility + * advice rather than allocation guarantees (MADV_NOHUGEPAGE blocks the + * normal fault-time, MADV_COLLAPSE and khugepaged paths; MADV_HUGEPAGE + * widens eligibility and increases defrag aggressiveness but does not + * force allocation, which still depends on the global + * transparent_hugepage=3D mode, VMA suitability, and allocation success= ); + * MADV_WIPEONFORK / MADV_KEEPONFORK do not wipe at fork time but cause + * the child's first access to fault in zero-filled pages; + * MADV_DONTDUMP / MADV_DODUMP normally control coredump inclusion but + * can be overridden by always_dump_vma() for gate, vm_ops-named or + * arch-named VMAs; and MADV_NORMAL / MADV_RANDOM / MADV_SEQUENTIAL are + * genuinely heuristic read-ahead hints. The non-destructive + * immediate-action group performs work + * synchronously while preserving page contents (MADV_WILLNEED, MADV_COL= D, + * MADV_PAGEOUT, MADV_POPULATE_READ, MADV_POPULATE_WRITE, MADV_COLLAPSE, + * MADV_GUARD_REMOVE). The destructive group discards, replaces or + * invalidates page contents (MADV_DONTNEED, MADV_DONTNEED_LOCKED, + * MADV_FREE, MADV_REMOVE, MADV_GUARD_INSTALL, MADV_HWPOISON, + * MADV_SOFT_OFFLINE). MADV_GUARD_INSTALL belongs to the destructive gro= up + * 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 down 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 update VMA flags. The flags fall into five + * sub-classes by how their consumers honor them: + * (a) Hard gates -- MADV_DONTFORK / MADV_DOFORK strictly gate VMA + * copy in dup_mmap(); MADV_MERGEABLE / MADV_UNMERGEABLE strictly + * gate whether KSM will scan the VMA at all. These take effect + * immediately and cannot be overridden by other policy. + * (b) THP eligibility advice -- MADV_NOHUGEPAGE blocks the normal + * fault-time, MADV_COLLAPSE and khugepaged THP paths for the VMA + * (driver-internal PMD insertion via insert_pmd() is the only + * documented bypass). MADV_HUGEPAGE only widens THP eligibility + * under the kernel's "madvise" / "except-advised" policy and + * increases defrag aggressiveness; it does not force allocation, + * which still depends on the global transparent_hugepage=3D mode + * (always / madvise / never), VMA suitability, defrag GFP policy, + * and allocation or memcg-charge success. + * (c) Fault-on-access -- MADV_WIPEONFORK / MADV_KEEPONFORK do not + * wipe pages at fork time; instead the child VMA's pages are not + * copied and the child sees zero-filled pages only when it first + * reads or writes them. + * (d) Mostly-strict with override -- MADV_DONTDUMP / MADV_DODUMP + * control coredump inclusion via VM_DONTDUMP, but always_dump_vma() + * can still include gate, vm_ops-named or arch-named VMAs in the + * core regardless. + * (e) Heuristic -- MADV_NORMAL / MADV_RANDOM / MADV_SEQUENTIAL set + * VM_RAND_READ / VM_SEQ_READ as read-ahead hints that the read-ahead + * code weighs against other policy and may diverge from at runtime. + * In all five sub-classes the requested flag bits on the VMA are se= t; + * what differs is the strength of the resulting downstream effect. + * + * - 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_page_range_single_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_page_range_single() 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; an + * unmapped gap inside the range causes the call to return -ENOMEM after + * processing the mapped portions, rather than aborting at the gap. + * + * POSIX defines posix_madvise(3) for a portable subset (POSIX_MADV_NORM= AL, + * _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 u= se + * 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 architectu= re, + * are cleared via untagged_addr() before the range is interpreted, wi= th + * 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 + * constraint-type: range(0, SIZE_MAX) + * cdesc: Length of the range in bytes. Internally rounded up to a multi= ple + * 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 . 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: Returned for invalid input (unrecognised MADV_*, Kconfig-gated + * behavior, unaligned start, range wrap, non-zero len_in rounding to + * zero) and for per-behavior VMA-filter violations. The constraint: + * blocks cover FREE, WIPEONFORK, REMOVE, COLD and PAGEOUT; inline + * filters also reject DOFORK on VM_SPECIAL, KEEPONFORK on VM_DROPPABL= E, + * DODUMP on non-hugetlb VM_SPECIAL/VM_DROPPABLE, GUARD_* on VM_SPECIAL + * or VM_HUGETLB, GUARD_INSTALL on VM_LOCKED. Also + * returned by faultin_page_range() and madvise_collapse_errno(). + * + * error: ENOMEM, Cannot allocate memory + * desc: Some part of the requested range falls in a gap between mapped + * VMAs; the kernel still applies the behavior to the mapped subranges + * and only returns -ENOMEM after the walk completes. MADV_POPULATE_* + * also returns -ENOMEM when the region has no VMA or when + * faultin_page_range() exhausts memory. MADV_COLLAPSE returns -ENOMEM + * when its struct collapse_control cannot be allocated up front, and + * when madvise_collapse_errno() maps SCAN_ALLOC_HUGE_PAGE_FAIL (no + * hugepage available) to -ENOMEM. + * + * error: EAGAIN, Resource temporarily unavailable + * desc: For the VMA-flag-mutating behaviors, an internal -ENOMEM from V= MA + * splitting is translated to -EAGAIN before being returned to userspa= ce, + * 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. MADV_WILLN= EED + * 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 is not a writable + * shared mapping (vma_is_shared_maywrite() is false). Punching a hole= in + * a private or read-only shared mapping is not permitted; the operati= on + * would either be invisible to other mappers or violate file permissi= ons. + * + * 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_REM= OVE, + * MADV_DONTFORK, MADV_WIPEONFORK, MADV_GUARD_INSTALL) is refused on a + * read-only anonymous VMA that has been sealed with mseal(2), to prev= ent + * 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). The single-shot + * madvise() syscall is not automatically restarted by the signal + * framework on this path; the caller must reissue the request if + * desired. + * + * error: EHWPOISON, Memory page has hardware error + * desc: MADV_POPULATE_READ or MADV_POPULATE_WRITE encountered a page th= at + * has been marked as containing a hardware-detected memory error and + * could not be faulted in. + * + * 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). + * + * 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. + * + * lock: mm->mmap_lock (read mode) + * type: rwlock + * acquired: yes + * released: yes + * desc: Held on entry to the VMA walk for MADV_REMOVE, MADV_WILLNEED, + * MADV_COLD, MADV_PAGEOUT and MADV_COLLAPSE, and as the fallback when + * the per-VMA fast path declines. Several handlers drop and reacquire + * this lock mid-operation: MADV_WILLNEED on a file-backed VMA and + * MADV_REMOVE around their vfs_fadvise() / vfs_fallocate() callouts; + * MADV_COLLAPSE on file-backed ranges around the page migration + * pipeline; MADV_POPULATE_* (dispatched directly to madvise_populate(= )) + * around each faultin_page_range() call, which may itself drop the + * lock internally before returning. + * + * lock: mm->mmap_lock (write mode; killable) + * type: rwlock + * 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_DO= DUMP, + * 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_lock) + * 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 + * 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 behaviors that establish page tables) 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 the sysca= ll + * wraps the per-VMA work in tlb_gather_mmu() / tlb_finish_mmu() so PTE + * clearing and TLB invalidation are batched. MADV_COLD and MADV_PAGEO= UT + * build a short-lived gather inside the handler. MADV_GUARD_INSTALL + * builds a transient gather via zap_page_range_single() each time the + * retry loop has to clear pre-existing pages; if the range is already + * empty no gather is built. MADV_GUARD_REMOVE never zaps and 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_page_range_single(), and + * MADV_FREE's own walk -- bracket their work with + * mmu_notifier_invalidate_range_start()/_end() so secondary MMUs (KVM, + * IOMMUv2, 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 surfacin= g as + * -EINTR to userspace. The single-shot madvise() syscall does not req= uest + * transparent restart on these paths; the caller is expected to reiss= ue + * the call if appropriate. + * errno: -EINTR + * timing: during + * restartable: no + * + * side-effect: modify_state + * target: vma->vm_flags + * condition: Hint-group behaviors (MADV_NORMAL, MADV_RANDOM, MADV_SEQUE= NTIAL, + * MADV_DONTFORK, MADV_DOFORK, MADV_DONTDUMP, MADV_DODUMP, MADV_WIPEON= FORK, + * MADV_KEEPONFORK, MADV_MERGEABLE, MADV_UNMERGEABLE, MADV_HUGEPAGE, + * MADV_NOHUGEPAGE) + * desc: Sets or clears VM_RAND_READ, VM_SEQ_READ, VM_DONTCOPY, VM_DONTD= UMP, + * VM_WIPEONFORK, VM_MERGEABLE or VM_HUGEPAGE on the affected VMAs and= may + * split or merge VMAs to apply the change to a sub-range. The change = is + * reversible by issuing madvise() with the inverse advice (e.g. + * MADV_DOFORK undoes MADV_DONTFORK), with the caveat that the inverse + * call's per-VMA filter still applies: MADV_DOFORK rejects VM_SPECIAL, + * MADV_DODUMP rejects non-hugetlb VM_SPECIAL or VM_DROPPABLE, and + * MADV_KEEPONFORK rejects VM_DROPPABLE; for those classes of VMA the + * inverse cannot complete. + * 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 underlying 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 pag= es + * may be reclaimed under memory pressure, while writes before + * reclamation cancel the lazy-free. Discarded data cannot be recovere= d. + * 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 h= ole + * 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 reclai= med + * sooner under pressure. MADV_PAGEOUT additionally calls reclaim_page= s() + * to write dirty pages out and drop clean ones synchronously. Page da= ta + * 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 n= ot + * fault. Equivalent to touching every page in the range while suppres= sing + * 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 pa= ge + * 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_page_range_single() 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 / soft_offline_page) + * condition: MADV_HWPOISON, MADV_SOFT_OFFLINE + * desc: MADV_HWPOISON marks the affected pages as containing an + * unrecoverable hardware error using the same machine-check path that + * real ECC failures take; MADV_SOFT_OFFLINE migrates the contents off + * the affected pages and removes them from the buddy allocator. Both + * paths affect physical memory bookkeeping kernel-wide and cannot be + * undone without a reboot. 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 the VMA's eligibility for the kernel same-page merger. + * Enabling merging may later cause identical anonymous pages to be + * replaced by shared, write-protected copies; disabling merging tears + * any existing merges down lazily. The flag toggle itself is reversib= le + * 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 userfaultfd-armed VMA + * desc: Generates a UFFD_EVENT_REMOVE notification covering the discard= ed + * 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)) =3D=3D 0 + * + * constraint: Length rounded up to PAGE_SIZE + * desc: The effective range length is PAGE_ALIGN(len_in). A non-zero le= n_in + * that overflows during rounding, or a (start, end) range that wraps, + * is rejected with -EINVAL. + * expr: end =3D start + PAGE_ALIGN(len_in); end >=3D 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) again= st + * 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 + * rejects file-backed VMAs with -EINVAL. + * expr: vma_is_anonymous(vma) + * + * constraint: MADV_WIPEONFORK requires private anonymous mappings + * desc: MADV_WIPEONFORK rejects file-backed mappings and shared anonymo= us + * 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 writable shared file mapping + * desc: MADV_REMOVE rejects VM_LOCKED VMAs, VMAs without an associated + * file/mapping/host inode, and non-shared-writable mappings. The first + * two cases return -EINVAL; a private or read-only shared mapping + * returns -EACCES. + * 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: madvise(2) reports only whether an error condition was + * encountered, not whether the requested work was performed. The hint + * group sets VMA flags whose downstream strictness varies: + * MADV_DONTFORK / MADV_DOFORK and MADV_MERGEABLE / MADV_UNMERGEABLE are + * hard gates honored by fork-copy and KSM scanning respectively; + * MADV_NOHUGEPAGE is a hard gate against the normal user-visible THP + * paths but MADV_HUGEPAGE is eligibility/advice that does not force + * THP installation -- the global transparent_hugepage=3D mode, VMA + * suitability and allocation success still apply; MADV_WIPEONFORK / + * MADV_KEEPONFORK take effect at the child's first page access + * (zero-on-fault), not at fork time; MADV_DONTDUMP / MADV_DODUMP gate + * coredump inclusion but can be overridden by always_dump_vma(); + * MADV_NORMAL / MADV_RANDOM / MADV_SEQUENTIAL are heuristic read-ahead + * hints that the read-ahead code may weigh against other policy. The + * non-hint behaviors + * are not uniform: walk-and-skip handlers (COLD, PAGEOUT, FREE, + * GUARD_REMOVE) silently skip pages that fail per-page preconditions; + * bulk-backend handlers (DONTNEED, DONTNEED_LOCKED, REMOVE, and + * WILLNEED on regular files) delegate the range to a single backend + * call whose return is propagated for REMOVE and discarded for + * WILLNEED; stop-on-error handlers (POPULATE_READ, POPULATE_WRITE, + * SOFT_OFFLINE) surface the first per-page failure rather than + * skipping; and hybrid handlers (WILLNEED for anon/shmem, COLLAPSE, + * GUARD_INSTALL, HWPOISON) mix walking with bulk backends, retry/zap + * loops or selective error suppression. A successful return therefore + * guarantees only that no error was raised in the handler that ran, + * not that every page was processed. + * + * 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_INSTAL= L / + * 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) extends the same set of advices 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. + * + * The discard subset (MADV_FREE, MADV_DONTNEED, MADV_DONTNEED_LOCKED, + * MADV_REMOVE, MADV_DONTFORK, MADV_WIPEONFORK, MADV_GUARD_INSTALL) is + * refused on non-writable anonymous VMAs sealed with mseal(2) on 64-bit + * kernels. mseal(2) does not provide an unseal operation, so applicatio= ns + * that need to retain the ability to discard such pages must keep the + * mapping writable (and arch-accessible for write) or refrain from + * sealing it. + * + * On a userfaultfd-armed VMA, all four destructive discards (DONTNEED, + * DONTNEED_LOCKED, FREE, REMOVE) emit a UFFD_EVENT_REMOVE event; the + * per-VMA fast path is bypassed and the syscall falls back to the + * heavier mmap_read_lock so the userfaultfd monitor is consulted before + * the discard takes effect. + * + * 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(); the kernel's syscall return path treats this as a + * transparent restart of madvise() and re-enters the call with the same + * arguments. The transparent restart is unconditional and is not driven + * by signal delivery, so the caller never observes an errno from this + * path 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 reser= ves + * MADV_SPACEAVAIL=3D5; portable code must use the symbolic names from + * . + */ SYSCALL_DEFINE3(madvise, unsigned long, start, size_t, len_in, int, behavi= or) { return do_madvise(current->mm, start, len_in, behavior); --=20 2.53.0 From nobody Mon Jun 8 09:48:26 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9DAFB3C5546; Fri, 29 May 2026 23:33:49 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097631; cv=none; b=ijWzPrKXhMXYHzqa4HeasaWvZegSbDvsAhVOYEhKhXDfbBe+kPkIPV//NoJesjQNXlNtdjiRRMHfVYBx/EuDzFp1JyP1ApoMFxFlHrXvWtlmlpSynRO4540mpb7t2f2ij59j/nMmjrkmsgzbNRWh8KlUhucAhh9Y2HQlgO+Y00I= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780097631; c=relaxed/simple; bh=hXTACtBIV6hiJQgdeXIpXE8oX0VfwYXpGLrdPdwrSB0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=TTMqRFntTERLYZjBiPP8P5NhyRP+8BEhx2p3+5HDfm3A/iZ7r0pS7zER5QGgDuUlULS/kKC3fiDSQDNl84jbvOs7Q0GueLH4Ax6ulAulhM0ZuMh91oXAgorVkp5MtMiFmzWr9hglVe+u4DSEELxKlx7F7hHqkP5GT4+zJmwq1bw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=Qxe1bqFi; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="Qxe1bqFi" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 673CE1F00899; Fri, 29 May 2026 23:33:46 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1780097629; bh=i5xoYOu+H/E2Qmd6vKoz6csEWSVaDrwB5J3ezvyhCxg=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=Qxe1bqFiXTODd2f4grnOICVXbhq2ocNkz+jxYbVEHDomrL78/pKUYx6CwgTDPf2vH yYmEnf7cSymZUlV7TX73zpm92q33c6jgFedQoR+P139razqoNlh+Vqu2FbcDUi/1ei aY5LEkRZvBGSGTExyqarQKrQAtrDiYkxXRa7Waa3cnrOC3qBH8L4aNhqW7VJWC1jAl 1s+e748Q14/FPA6VAOCaaexu3U1xzaXCgjTHrL1mjdt3aXzWqO+/TidI2GkfNCHHFJ +lnMI48vCXplKCo+YH3PTS4B8U3HK6FmSkgl5VuEkIH2VPTy38q5sjCJA4KH8dNTWa ZxJTWsYDgAo1g== From: Sasha Levin To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org, Thomas Gleixner , "Paul E . McKenney" , Greg Kroah-Hartman , Jonathan Corbet , Dmitry Vyukov , Randy Dunlap , Cyril Hrubis , Kees Cook , Jake Edge , David Laight , Gabriele Paoloni , Mauro Carvalho Chehab , Christian Brauner , Alexander Viro , Andrew Morton , Masahiro Yamada , Shuah Khan , Arnd Bergmann , Nathan Chancellor , Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Subject: [PATCH v4 11/11] kernel/api: add syscall enter/exit tracepoints Date: Fri, 29 May 2026 19:33:10 -0400 Message-ID: <20260529233311.1901670-12-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260529233311.1901670-1-sashal@kernel.org> References: <20260529233311.1901670-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add two tracepoints to the CONFIG_KAPI_RUNTIME_CHECKS syscall validation path so the framework's behavior can be observed without the noise and loss of pr_warn_ratelimited(): kapi_syscall_enter - the spec name, the raw argument values, and a rendered "name=3Dvalue" list of the specified parameters (pointer-like values in hex, integers and file descriptors in decimal) kapi_syscall_exit - the spec name, the return value, and whether it matched the specification (spec_match) Both fire only for syscalls that have a KAPI specification and live inside the existing CONFIG_KAPI_RUNTIME_CHECKS region, so they exist exactly when the runtime checks do; they compile to no-ops without CONFIG_TRACEPOINTS and stay dormant until enabled. The parameter list is rendered only when the enter tracepoint is enabled. kapi_syscall_exit is also emitted on the parameter-validation rejection path -- where the validator returns -EINVAL and the real handler is skipped -- with spec_match=3D0, so every kapi_syscall_enter has a matching exit. Signed-off-by: Sasha Levin --- Documentation/dev-tools/kernel-api-spec.rst | 29 ++++++++ MAINTAINERS | 1 + include/trace/events/kapi.h | 74 ++++++++++++++++++++ kernel/api/kernel_api_spec.c | 77 ++++++++++++++++++--- 4 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 include/trace/events/kapi.h diff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/de= v-tools/kernel-api-spec.rst index 26598a98c0f69..561e7bff58379 100644 --- a/Documentation/dev-tools/kernel-api-spec.rst +++ b/Documentation/dev-tools/kernel-api-spec.rst @@ -285,6 +285,35 @@ custom validation functions via the ``validate`` field= in the constraint spec: .type =3D KAPI_CONSTRAINT_CUSTOM, .validate =3D validate_buffer_size, =20 +Tracepoints +----------- + +When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, the syscall validation pat= h emits +two ftrace tracepoints (in the ``kapi`` trace system) for every syscall th= at has a +specification: + +- ``kapi_syscall_enter`` -- fired before parameter validation, recording t= he spec + name, the raw syscall argument values, and -- when the spec provides par= ameter + metadata -- a rendered ``name=3Dvalue`` list: pointer-like values are sh= own in hex, + integers and file descriptors in decimal, 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 skipp= ed and + ``-EINVAL`` is returned). Records the spec name, the return value, and + ``spec_match``: 0 when the call did not conform to the spec -- the param= eters were + rejected, or the return value was not one the spec allows -- and 1 other= wise. + +Unlike the ``pr_warn_ratelimited`` violation reports, the tracepoints capt= ure every +spec'd call rather than only violations, are lossless under load, 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=3D3, buf=3D0x7ffd46780b58, count= =3D0x340) + ... kapi_syscall_exit: sys_read =3D 832 spec_match=3D1 + ... kapi_syscall_enter: sys_open(filename=3D0x480300, flags=3D268435= 456, mode=3D0x0) + ... kapi_syscall_exit: sys_open =3D -22 spec_match=3D0 + DebugFS Interface =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D =20 diff --git a/MAINTAINERS b/MAINTAINERS index ddfd9cad98916..48def631ad823 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13823,6 +13823,7 @@ L: linux-api@vger.kernel.org S: Maintained F: Documentation/dev-tools/kernel-api-spec.rst 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 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 + +/* Max length of the rendered "name=3Dvalue, ..." 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, w= hen + * the spec provides parameter metadata, a rendered "name=3Dvalue" list (p= arams, + * built by the caller): pointer-like values in hex, integers and fds in d= ecimal. + */ +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 =3D 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 =3D ret; + __entry->spec_match =3D spec_match; + ), + + TP_printk("%s =3D %ld spec_match=3D%d", + __get_str(name), __entry->ret, __entry->spec_match) +); + +#endif /* _TRACE_KAPI_H */ + +/* This part must be outside protection */ +#include diff --git a/kernel/api/kernel_api_spec.c b/kernel/api/kernel_api_spec.c index 1a9041a7f21a4..2aa8c04a5851e 100644 --- a/kernel/api/kernel_api_spec.c +++ b/kernel/api/kernel_api_spec.c @@ -659,6 +659,45 @@ EXPORT_SYMBOL_GPL(kapi_print_spec); =20 #ifdef CONFIG_KAPI_RUNTIME_CHECKS =20 +#define CREATE_TRACE_POINTS +#include + +/* + * Render a syscall's parameters as a "name=3Dvalue, ..." 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 =3D 0; + + buf[0] =3D '\0'; + /* Bound by the caller-supplied arg count; the spec arity may differ. */ + for (i =3D 0; args && i < nargs && i < 6; i++) { + const char *name =3D "arg"; + bool dec =3D false; + + if (i < spec->param_count) { + const struct kapi_param_spec *ps =3D &spec->params[i]; + + if (ps->name) + name =3D ps->name; + dec =3D ps->type =3D=3D KAPI_TYPE_INT || ps->type =3D=3D KAPI_TYPE_FD; + } + + used +=3D scnprintf(buf + used, size - used, "%s%s=3D", + i ? ", " : "", name); + if (dec) + used +=3D scnprintf(buf + used, size - used, "%lld", + (long long)args[i]); + else + used +=3D scnprintf(buf + used, size - used, "0x%llx", + (unsigned long long)args[i]); + } +} + /** * kapi_validate_fd - Validate that a file descriptor value is in valid ra= nge * @fd: File descriptor to validate @@ -1154,16 +1193,24 @@ EXPORT_SYMBOL_GPL(kapi_validate_syscall_param); int kapi_validate_syscall_params(const struct kernel_api_spec *spec, const s64 *params, int param_count) { - int i; + int i, ret =3D 0; =20 if (!spec || !params) return 0; =20 + 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 !=3D spec->param_count) { pr_warn_ratelimited("API %s: parameter count mismatch (expected %u, got = %d)\n", spec->name, spec->param_count, param_count); - return -EINVAL; + ret =3D -EINVAL; + goto out; } =20 /* Validate each parameter with context */ @@ -1173,12 +1220,22 @@ int kapi_validate_syscall_params(const struct kerne= l_api_spec *spec, if (!kapi_validate_param_with_context(param_spec, params[i], params, par= am_count)) { if (strncmp(spec->name, "sys_", 4) =3D=3D 0) { /* For syscalls, we can return EINVAL to userspace */ - return -EINVAL; + ret =3D -EINVAL; + goto out; } } } =20 - return 0; +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); =20 @@ -1301,14 +1358,18 @@ EXPORT_SYMBOL_GPL(kapi_validate_return_value); */ int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 r= etval) { + bool valid =3D true; + if (!spec) return 0; =20 - /* Skip return validation if return spec was not defined */ - if (spec->return_magic !=3D KAPI_MAGIC_RETURN) - return 0; + /* Validate against the return spec when one was defined */ + if (spec->return_magic =3D=3D KAPI_MAGIC_RETURN) + valid =3D kapi_validate_return_value(spec, retval); + + trace_kapi_syscall_exit(spec->name, retval, valid); =20 - if (!kapi_validate_return_value(spec, retval)) { + if (!valid) { /* Log the violation but don't change the return value */ pr_warn_ratelimited("KAPI: Syscall %s returned unspecified value %lld\n", spec->name, retval); --=20 2.53.0