From: John Garry In [0], a fix was proposed for the NS and NS head (multipath) cdev lifetime. The issue was that fds for the nvme-generic cdev may exist after we tear down the nvme-subsystem. Issuing an ioctl on that cdev may expose a use-after-free. This test recreates the method described in [0] to expose this issue for NS head cdev. First a fd is created by opening the nvme-generic cdev. Next we tear down the nvme-subsystem. Finally we try to issue an ioctl on the cdev - without the kernel fix in [0], this should trigger a KASAN warn. [0] https://lore.kernel.org/linux-nvme/20260713104238.3034640-1-john.g.garry@oracle.com/T/#me118851584fbcbb960795d0f04e2262e5a295613 Signed-off-by: John Garry --- Difference to v1: - improve shell script formatting diff --git a/common/nvme b/common/nvme index 15e9c3f..902a33f 100644 --- a/common/nvme +++ b/common/nvme @@ -300,6 +300,26 @@ _nvme_wait_subsys_removed() { done } +_nvme_find_subsys_nvme_generic() { + local subsysnqn="$def_subsysnqn" + + for subsyspath in /sys/class/nvme-subsystem/*; do + _subsysnqn=$(cat "${subsyspath}/subsysnqn" 2> /dev/null) + if [ "$subsysnqn" == "$_subsysnqn" ]; then + subsyspathbase="$(basename "$subsyspath")" + for nvmegeneric in /sys/class/nvme-generic/*; do + nvmegenericbase="$(basename "$nvmegeneric")" + devicelink="$(readlink "$nvmegeneric/device")" + if [[ "$devicelink" =~ $subsyspathbase ]]; then + echo "$nvmegenericbase" + break + fi + done + break + fi + done +} + _nvme_connect_subsys() { local subsysnqn="$def_subsysnqn" local hostnqn="$def_hostnqn" diff --git a/src/.gitignore b/src/.gitignore index 988d785..af9ebf6 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -14,3 +14,4 @@ /nvme-passthrough-meta /ioctl-lbmd-query /nvme-passthru-admin-uring +/nvme-delay-ioctl diff --git a/src/Makefile b/src/Makefile index d75daf1..4779ffd 100644 --- a/src/Makefile +++ b/src/Makefile @@ -18,6 +18,7 @@ C_TARGETS := \ loop_change_fd \ loop_get_status_null \ mount_clear_sock \ + nvme-delay-ioctl \ nvme-passthrough-meta \ ioctl-lbmd-query \ nbdsetsize \ diff --git a/src/nvme-delay-ioctl.c b/src/nvme-delay-ioctl.c new file mode 100644 index 0000000..8699afa --- /dev/null +++ b/src/nvme-delay-ioctl.c @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0+ + +/* + * Simple test to issue an ioctl some time after opening a cdev. + * This is to test lifetime of cdev, and whether we can handle + * removing the cdev while we have active fds. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef _LINUX_NVME_IOCTL_H +#define _LINUX_NVME_IOCTL_H +#define NVME_IOCTL_ID _IO('N', 0x40) +#endif /* _UAPI_LINUX_NVME_IOCTL_H */ + +int main(int argc, char **argv) +{ + int fd, fd1; + int count; + + int delay_seconds = 2; + + if (argc < 2) { + fprintf(stderr, "usage: %s /dev/ngXnX", argv[0]); + return EINVAL; + } + + fd = open(argv[1], O_RDONLY); + if (fd < 0) + return fd; + + /* + * Steps: + * a. Try to open device until disallowed/gone + * b. sleep to allow nvme-subsystem be torn down + * c. issue the ioctl on original fd + */ + count = 0; + for (;;) { + fd1 = open(argv[1], O_RDONLY); + usleep(500000); + if (fd1 < 0) + break; + close(fd1); + count++; + if (count > 10) { + fprintf(stderr, "%s still present", argv[0]); + return EINVAL; + } + } + sleep(delay_seconds); + ioctl(fd, NVME_IOCTL_ID); + close(fd); + + return 0; +} diff --git a/tests/nvme/069 b/tests/nvme/069 new file mode 100755 index 0000000..46c3e5a --- /dev/null +++ b/tests/nvme/069 @@ -0,0 +1,49 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-3.0+ +# Copyright (C) 2026 John Garry +# +# Test NVMe multipath nvme-generic cdev lifetime behaves as expected. + +. tests/nvme/rc + +DESCRIPTION="NVMe multipath cdev lifetime test" +CHECK_DMESG=1 + +requires() { + _nvme_requires + _have_loop + _have_module_param_value nvme_core multipath Y + _require_nvme_trtype_is_fabrics + _have_kernel_options KASAN +} + +set_conditions() { + _set_nvme_trtype "$@" +} + +test() { + echo "Running ${TEST_NAME}" + + _setup_nvmet + + local nvmedev + _nvmet_target_setup + + _nvme_connect_subsys + + nvmedev=$(_find_nvme_dev "${def_subsysnqn}") + + nvmegeneric=$(_nvme_find_subsys_nvme_generic) + + # nvme-delay-ioctl will open the nvme-generic cdev, sleep, and then + # issue an ioctl. + # We tear down the nvme-subsystem in the foreground so we can catch + # if the ioctl triggers a use-after-free KASAN warn. + src/nvme-delay-ioctl "/dev/$nvmegeneric" & + sleep 0.5 + _nvme_disconnect_ctrl "${nvmedev}" + _nvmet_target_cleanup + wait < <(jobs -p) + + echo "Test complete" +} diff --git a/tests/nvme/069.out b/tests/nvme/069.out new file mode 100644 index 0000000..cd2e1fd --- /dev/null +++ b/tests/nvme/069.out @@ -0,0 +1,2 @@ +Running nvme/069 +Test complete -- 2.43.7