From cd55dd717f8e2e5f7913b814fdf2bad1d5b50244 Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Mon, 20 Apr 2026 17:12:25 -0400 Subject: [PATCH 1/2] libnvme: add missing prototypes for exported symbols Three __public symbols were listed in the version scripts but had no prototype in any installed header: libnvme_mi_submit_entry() -- weak tracing hook in mi.c libnvme_mi_submit_exit() -- weak tracing hook in mi.c libnvmf_context_set_fabrics_config() -- setter in fabrics.c whose getter libnvmf_context_get_fabrics_config() was already declared Without a prototype in an installed header, callers have no declaration to include and must either write their own or resort to dlsym(). Signed-off-by: Martin Belanger Assisted-by: Claude Sonnet 4.6 --- libnvme/src/nvme/fabrics.h | 13 +++++++++++++ libnvme/src/nvme/mi.h | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/libnvme/src/nvme/fabrics.h b/libnvme/src/nvme/fabrics.h index f736c3c543..f09f0d118f 100644 --- a/libnvme/src/nvme/fabrics.h +++ b/libnvme/src/nvme/fabrics.h @@ -404,6 +404,19 @@ int libnvmf_context_set_device(struct libnvmf_context *fctx, const char *device) struct libnvme_fabrics_config *libnvmf_context_get_fabrics_config( struct libnvmf_context *fctx); +/** + * libnvmf_context_set_fabrics_config() - Set fabrics configuration for a + * fabrics context + * @fctx: Fabrics context + * @cfg: Fabrics configuration to apply + * + * Copies the fields of @cfg into the fabrics configuration of @fctx. + * + * Return: 0 on success, or a negative error code on failure. + */ +int libnvmf_context_set_fabrics_config(struct libnvmf_context *fctx, + struct libnvme_fabrics_config *cfg); + /** * libnvmf_ctrl_get_fabrics_config() - Fabrics configuration of a controller * @c: Controller instance diff --git a/libnvme/src/nvme/mi.h b/libnvme/src/nvme/mi.h index d1d184c126..7704af9f28 100644 --- a/libnvme/src/nvme/mi.h +++ b/libnvme/src/nvme/mi.h @@ -947,3 +947,37 @@ int libnvme_mi_aem_disable(libnvme_mi_ep_t ep); * Return: 0 is a success, nonzero is an error and errno may be read for further details */ int libnvme_mi_aem_process(libnvme_mi_ep_t ep, void *userdata); + +/** + * libnvme_mi_submit_entry() - Weak hook called before an MI message is sent. + * @type: MCTP message type + * @hdr: Pointer to the MI message header + * @hdr_len: Length of the message header in bytes + * @data: Pointer to message payload data + * @data_len: Length of payload data in bytes + * + * This is a weak symbol that can be overridden by an application to intercept + * outgoing MI messages for tracing or testing purposes. The return value is + * passed back as @user_data to the matching libnvme_mi_submit_exit() call. + * + * Return: An opaque pointer passed to libnvme_mi_submit_exit(), or NULL. + */ +void *libnvme_mi_submit_entry(__u8 type, const struct nvme_mi_msg_hdr *hdr, + size_t hdr_len, const void *data, size_t data_len); + +/** + * libnvme_mi_submit_exit() - Weak hook called after an MI message completes. + * @type: MCTP message type + * @hdr: Pointer to the MI response message header + * @hdr_len: Length of the response message header in bytes + * @data: Pointer to response payload data + * @data_len: Length of response payload data in bytes + * @user_data: Value returned by the matching libnvme_mi_submit_entry() call + * + * This is a weak symbol that can be overridden by an application to intercept + * completed MI transactions. Called with the opaque pointer returned by the + * corresponding libnvme_mi_submit_entry() call. + */ +void libnvme_mi_submit_exit(__u8 type, const struct nvme_mi_msg_hdr *hdr, + size_t hdr_len, const void *data, size_t data_len, + void *user_data); From a184fa5afd88cd279c21d58dea490dff19b701ab Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Mon, 20 Apr 2026 17:12:44 -0400 Subject: [PATCH 2/2] libnvme: add check-public-headers test Add tools/check-public-headers.py, a companion to the existing check-public-symbols.py, that verifies every symbol exported in a version script has a prototype declared in one of the installed headers. The check reads all *.ld files, extracts their global: symbols, then scans each installed header for a matching identifier followed by '('. Conditional headers (MI, fabrics) are silently skipped when absent, matching the same build conditionality as install_headers(). Register the script as a meson test alongside check-public-symbols so it runs automatically under 'meson test'. Signed-off-by: Martin Belanger Assisted-by: Claude Sonnet 4.6 --- libnvme/test/meson.build | 13 +++ libnvme/tools/check-public-headers.py | 115 ++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 libnvme/tools/check-public-headers.py diff --git a/libnvme/test/meson.build b/libnvme/test/meson.build index bdb815498c..7e2b9cc0b8 100644 --- a/libnvme/test/meson.build +++ b/libnvme/test/meson.build @@ -15,6 +15,19 @@ if python3_prog.found() meson.project_source_root() / 'libnvme', ], ) + _ld_args = ['--ld', nvme_ld, '--ld', accessors_ld] + if want_fabrics + _ld_args += ['--ld', nvmf_ld, '--ld', nvmf_accessors_ld] + endif + _hdr_args = [] + foreach hdr : headers + _hdr_args += ['--header', meson.project_source_root() / 'libnvme' / 'src' / hdr] + endforeach + test( + 'libnvme - check-public-headers', + python3_prog, + args: [files('../tools/check-public-headers.py')] + _ld_args + _hdr_args, + ) endif # These tests all require interaction with a real NVMe device, so we don't diff --git a/libnvme/tools/check-public-headers.py b/libnvme/tools/check-public-headers.py new file mode 100644 index 0000000000..c5c1f31f82 --- /dev/null +++ b/libnvme/tools/check-public-headers.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of libnvme. +# Copyright (c) 2025, Dell Technologies Inc. or its subsidiaries. +# +# Authors: Martin Belanger +# +# Verify that every symbol exported in a version script has a prototype +# declared in one of the installed header files. +# +# A __public function that appears in a .ld version script but not in any +# installed header is technically callable by external code, but callers have +# no declaration to include — they would need to write their own prototype or +# use dlsym(), which defeats the purpose of a stable public API. +# +# Usage (via meson — preferred, keeps meson.build as single source of truth): +# python3 tools/check-public-headers.py \ +# --ld src/libnvme.ld --ld src/accessors.ld [--ld ...] \ +# --header src/nvme/lib.h --header src/nvme/tree.h [--header ...] +# +# Usage (standalone, auto-discovers files from the source root): +# python3 tools/check-public-headers.py [LIBNVME-SOURCE-ROOT] +# +# In auto-discovery mode the script scans src/*.ld for version scripts and +# src/nvme/*.h (excluding files whose name contains "private") for headers. +# The source root defaults to the parent directory of this script. + +import argparse +import re +import sys +import pathlib + + +def parse_args(): + parser = argparse.ArgumentParser( + description='Check that every exported symbol has a prototype in an ' + 'installed header.') + parser.add_argument( + 'root', nargs='?', + help='libnvme source root for auto-discovery (defaults to the parent ' + 'of this script); ignored when --ld / --header are given') + parser.add_argument( + '--ld', action='append', metavar='FILE', dest='ld_files', + help='version-script (.ld) file to read exported symbols from ' + '(may be repeated)') + parser.add_argument( + '--header', action='append', metavar='FILE', dest='headers', + help='installed header file to search for prototypes ' + '(may be repeated)') + return parser.parse_args() + + +def main(): + args = parse_args() + + if args.ld_files or args.headers: + if not args.ld_files or not args.headers: + sys.exit('error: --ld and --header must both be provided together') + ld_files = [pathlib.Path(f) for f in args.ld_files] + headers = [pathlib.Path(f) for f in args.headers] + else: + root = pathlib.Path(args.root) if args.root else \ + pathlib.Path(__file__).resolve().parent.parent + src = root / 'src' + ld_files = sorted(src.glob('*.ld')) + headers = sorted(h for h in (src / 'nvme').glob('*.h') + if 'private' not in h.name) + + # ----------------------------------------------------------------------- + # Collect all symbols listed in the version scripts + # ----------------------------------------------------------------------- + ld_syms = {} # symbol -> Path of the .ld file that declares it + + for ld_path in ld_files: + for line in ld_path.read_text().splitlines(): + m = re.match(r'^\s+([a-z]\w+);', line) + if m: + ld_syms[m.group(1)] = ld_path + + # ----------------------------------------------------------------------- + # Collect all names that appear as a prototype/declaration in installed + # headers. Match any identifier immediately followed by '(' — this + # catches both single-line and multi-line function declarations, and macro + # definitions that alias a function name. The libnvme_*/libnvmf_* + # namespace is long enough that false positives from comments are not a + # practical concern. + # ----------------------------------------------------------------------- + header_syms = set() + + for hdr_path in headers: + for m in re.finditer(r'\b([a-z_]\w+)\s*\(', hdr_path.read_text()): + header_syms.add(m.group(1)) + + # ----------------------------------------------------------------------- + # Report exported symbols with no prototype in any installed header + # ----------------------------------------------------------------------- + errors = 0 + + for sym, ld_path in sorted(ld_syms.items()): + if sym not in header_syms: + print(f'ERROR: {sym}() is exported in {ld_path.name} ' + f'but has no prototype in any installed header') + errors += 1 + + if errors: + print(f'\n{errors} error(s) found.') + sys.exit(1) + + print(f'OK: all {len(ld_syms)} exported symbols have prototypes ' + f'in installed headers.') + + +if __name__ == '__main__': + main()