From nobody Mon Jun 15 06:29:58 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (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 88E8737C0F9; Wed, 8 Apr 2026 15:23:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1775661822; cv=none; b=RnJnwYklt8dAF4RwvLe6cRce/DdavgEDlRQDJOwylXnlrIsxCpbxnCC769nOfp7pKcLx/GdfXZb563/LisYaG9Onj0wsiBks6rSav9XySft7M+xyqhZkYRcVe7o4oG2dnWMUS+FUFnnWRL0dNxoTQdbLSE8QrXY+zqxU5xFx4rg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1775661822; c=relaxed/simple; bh=xy2wTzXh3oJ06N9hNsWTCm7gzKhOvMebokWJWOugQTM=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=io3iXwYnw7+ICliNYlVq5tEp6G4f1ir+c7oKf68NsA5anWsZUaiWoXoqfqFWrS5E+DTOoxVsuy0iPyQMDVu+tuFKY4pQRgOaCdaM5TALOdQdY4OcEWscM8dFM9K0fVb0YRSfbSGpy9BbeLt0V32EYjWqDfnNiHu7IKrIZ2R3S40= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=UTOXOO49; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="UTOXOO49" Received: by smtp.kernel.org (Postfix) with ESMTPS id 38B06C2BC87; Wed, 8 Apr 2026 15:23:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775661822; bh=xy2wTzXh3oJ06N9hNsWTCm7gzKhOvMebokWJWOugQTM=; h=From:Date:Subject:References:In-Reply-To:To:Cc:Reply-To:From; b=UTOXOO49luheuly8E3TpkfDK9w3e3F95JIZ2I2R7besK2uRFjB+SPI4OPM7PgD+GI JYtG3dlYq5nsfiNa81jf8IUoydrdzZxvWduZlZZVXlCl0V6F3MzluMSoY46USUHwwZ ZkXU4CMzAYCXZJJUaZ4ZDkcYEVNCYjzUxl+kN3MIA9AoA4WElarHzac7S+n+5fLm8l SqPPCMI4scX3sQMqwubx8VxukKwhZ5bXqfGgPolZ/LQL8S/45DH2G4xxqEgodgxOGA g09HOurOd6TP2PGC6+29JoQ0EfJDDoCy+he7iVtOxXQaBKMA0831dt3NqSW39kx8K/ 99K+EA47dqikQ== Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 23B4210F994F; Wed, 8 Apr 2026 15:23:42 +0000 (UTC) From: Alberto Ruiz via B4 Relay Date: Wed, 08 Apr 2026 17:23:40 +0200 Subject: [PATCH 1/2] fuse: fix device node leak in cuse_process_init_reply() 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 Message-Id: <20260408-wip-cuse-leak-fix-v1-1-1c028d575e97@redhat.com> References: <20260408-wip-cuse-leak-fix-v1-0-1c028d575e97@redhat.com> In-Reply-To: <20260408-wip-cuse-leak-fix-v1-0-1c028d575e97@redhat.com> To: Miklos Szeredi , Shuah Khan Cc: linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Alberto Ruiz X-Mailer: b4 0.14.2 X-Developer-Signature: v=1; a=ed25519-sha256; t=1775661821; l=2016; i=aruiz@redhat.com; s=20260408; h=from:subject:message-id; bh=FFr25cv2qmdGgRJVSTfidJQebQx41SqD8R/u4bhxMxY=; b=6xlTlB+2IVAzx1rejwjK5k5wUp0x1dIltBb9ymPiEaWSaoXWs9X/D8pXbOqQqnTCaJ+eQCKCV C51YT2d1/04BUzCgSE2dOzMaZ2x4Hx1ZrByqauumjuSkgMK4rl8BeQO X-Developer-Key: i=aruiz@redhat.com; a=ed25519; pk=ZJyUFl31NDNjeRWoQa/6I6bV5lb8axrDvpj9uGd+NvU= X-Endpoint-Received: by B4 Relay for aruiz@redhat.com/20260408 with auth_id=727 X-Original-From: Alberto Ruiz Reply-To: aruiz@redhat.com From: Alberto Ruiz If device_add() succeeds during CUSE initialization but a subsequent step (cdev_alloc() or cdev_add()) fails, the error path calls put_device() without first calling device_del(). This leaks the devtmpfs entry created by device_add(), leaving a stale /dev/ node that persists until reboot. Since the cuse_conn is never linked into cuse_conntbl on the failure path, cuse_channel_release() sees cc->dev =3D=3D NULL and skips device_unregister(), so no other code path cleans up the node. This has several consequences: - The device name is permanently poisoned: any subsequent attempt to create a CUSE device with the same name hits the stale sysfs entry, device_add() fails, and the new device is aborted. - The collision manifests as ENODEV returned to userspace with no dmesg diagnostic, making it very difficult to debug. - The failure is self-perpetuating: once a name is leaked, all future attempts with that name fail identically. Fix this by introducing an err_dev label that calls device_del() to undo device_add() before falling through to err_unlock. The existing err_unlock path from a device_add() failure correctly skips device_del() since the device was never added. Signed-off-by: Alberto Ruiz --- fs/fuse/cuse.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/fuse/cuse.c b/fs/fuse/cuse.c index dfcb98a654d83bb34460a6e8c9e4b196dfdfdfbe..df9d50a9c0fab269102ec0e4b2d= 459ca2a390c59 100644 --- a/fs/fuse/cuse.c +++ b/fs/fuse/cuse.c @@ -391,7 +391,7 @@ static void cuse_process_init_reply(struct fuse_mount *= fm, rc =3D -ENOMEM; cdev =3D cdev_alloc(); if (!cdev) - goto err_unlock; + goto err_dev; =20 cdev->owner =3D THIS_MODULE; cdev->ops =3D &cuse_frontend_fops; @@ -417,6 +417,8 @@ static void cuse_process_init_reply(struct fuse_mount *= fm, =20 err_cdev: cdev_del(cdev); +err_dev: + device_del(dev); err_unlock: mutex_unlock(&cuse_lock); put_device(dev); --=20 2.52.0 From nobody Mon Jun 15 06:29:58 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (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 88DF735B632; Wed, 8 Apr 2026 15:23:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1775661822; cv=none; b=oRsQGmZCaBSmzMy4JEjk4jj93t1PPKMYVh4CBgRuCDrRAaX06KzzBtKCIPBF/+yJrzCCh32lk3vOvmMZR7AFoED/ud/jCC4CFMqrDyCkIii8LvHFF/f6mprKrmXVx7ZQbHQSolKv5oW4B9eA3Q1Das6cjTN2psRnKozW1kAsvRo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1775661822; c=relaxed/simple; bh=RI6VZ6IzjOyhE67VI/e+612rJ5QwLMIiMfqJKNxbziw=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=MdLi7+GEskqRHqsLF6LTbIIDnO2N/Lmq96oKGzfDCQlWu63kOp5oVKYgnSpqD4MleTZiY+ftQgKo8rUZ43AtNwLZote/3tJ/xXFKhXctEvD/l/KQzom8NeHiSrSpVMXIiaW3yH9XdWIIYWEx15SLe4ClpJFLV3/Fhl2zMJqUIeo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=EbFWLpGU; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="EbFWLpGU" Received: by smtp.kernel.org (Postfix) with ESMTPS id 4AB3CC2BC9E; Wed, 8 Apr 2026 15:23:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775661822; bh=RI6VZ6IzjOyhE67VI/e+612rJ5QwLMIiMfqJKNxbziw=; h=From:Date:Subject:References:In-Reply-To:To:Cc:Reply-To:From; b=EbFWLpGUmmPosdWTc9b6EqkJtd80IAIJGzEeGEyTL0EBSS/Q0nxLY4EgV6A41NwvQ HfHF0Qqi0CSmINkwtiDW2PVKVo+nku8Ilv8K2TBDp5cOa0JuR4PC6mA8C/+phlS3Oe JAy0I8mUsuvWN6stJGB0mhvWrYWMB+8pukKp8dr9m/OdTV6fO4K1ykJwoqVf+MQO8X 4qD+UJu3JcKhDQMjNlNCACiFC6HKU1WH06qjhR0nxOStjYSABKJjiCrLfhGxS5FP9V r3oyMg8nB9aeIN0ohmDcktxw47noDldsxXQCD9gXiBCd38n5NiK95IPJdcyH99ZFVt CAkp0+wImkg4Q== Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 34F1E10F9956; Wed, 8 Apr 2026 15:23:42 +0000 (UTC) From: Alberto Ruiz via B4 Relay Date: Wed, 08 Apr 2026 17:23:41 +0200 Subject: [PATCH 2/2] selftests: add CUSE device-node leak regression test 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 Message-Id: <20260408-wip-cuse-leak-fix-v1-2-1c028d575e97@redhat.com> References: <20260408-wip-cuse-leak-fix-v1-0-1c028d575e97@redhat.com> In-Reply-To: <20260408-wip-cuse-leak-fix-v1-0-1c028d575e97@redhat.com> To: Miklos Szeredi , Shuah Khan Cc: linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Alberto Ruiz X-Mailer: b4 0.14.2 X-Developer-Signature: v=1; a=ed25519-sha256; t=1775661821; l=14239; i=aruiz@redhat.com; s=20260408; h=from:subject:message-id; bh=0ZY4XDFugXC4mOyITsG5+8zs1aPifzJy5hp1+ahuT3k=; b=njiGMmLeYWD5RJ7m4cgaStLasPPgfuZ7aTDa76wPPTEXFv/sxUaHzLjW6McbGnqQnLG+w015b 1Zw32N3WRyTBlNPK+nxo9TxifyHIix9YCeK72mpTt05qnsmwfGS/yM5 X-Developer-Key: i=aruiz@redhat.com; a=ed25519; pk=ZJyUFl31NDNjeRWoQa/6I6bV5lb8axrDvpj9uGd+NvU= X-Endpoint-Received: by B4 Relay for aruiz@redhat.com/20260408 with auth_id=727 X-Original-From: Alberto Ruiz Reply-To: aruiz@redhat.com From: Alberto Ruiz Add a kselftest under filesystems/cuse that verifies the error path in cuse_process_init_reply() properly calls device_del() when cdev_alloc() fails after device_add() has succeeded. The test has two subtests: 1. normal_cleanup: performs a complete CUSE_INIT handshake, verifies the device node appears, closes the channel fd, and verifies the node is removed. Basic sanity check. 2. leak_regression: forces cdev_alloc() to fail and verifies the /dev node does not leak. Uses the cuse_inject_cdev_failure module parameter (preferred) or failslab with stack-trace filtering (fallback). A CONFIG_FAULT_INJECTION-guarded module parameter is added to fs/fuse/cuse.c to allow the test to deterministically force cdev_alloc() failure. This is more reliable than failslab alone, which depends on the stack unwinder producing matching addresses (known to fail under UML). Signed-off-by: Alberto Ruiz --- fs/fuse/cuse.c | 13 + tools/testing/selftests/Makefile | 1 + tools/testing/selftests/filesystems/cuse/Makefile | 7 + tools/testing/selftests/filesystems/cuse/config | 5 + .../selftests/filesystems/cuse/cuse_leak_test.c | 406 +++++++++++++++++= ++++ 5 files changed, 432 insertions(+) diff --git a/fs/fuse/cuse.c b/fs/fuse/cuse.c index df9d50a9c0fab269102ec0e4b2d459ca2a390c59..5212a2290581ad9ffbe4b9bdac3= e6fbc6cf5484a 100644 --- a/fs/fuse/cuse.c +++ b/fs/fuse/cuse.c @@ -54,6 +54,13 @@ #include "fuse_i.h" #include "fuse_dev_i.h" =20 +#ifdef CONFIG_FAULT_INJECTION +static bool cuse_inject_cdev_failure; +module_param(cuse_inject_cdev_failure, bool, 0644); +MODULE_PARM_DESC(cuse_inject_cdev_failure, + "Force cdev_alloc() failure in CUSE init (test only)"); +#endif + #define CUSE_CONNTBL_LEN 64 =20 struct cuse_conn { @@ -390,6 +397,12 @@ static void cuse_process_init_reply(struct fuse_mount = *fm, /* register cdev */ rc =3D -ENOMEM; cdev =3D cdev_alloc(); +#ifdef CONFIG_FAULT_INJECTION + if (cuse_inject_cdev_failure && cdev) { + kobject_put(&cdev->kobj); + cdev =3D NULL; + } +#endif if (!cdev) goto err_dev; =20 diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Mak= efile index 450f13ba4cca98836bc8a2fe18a2eb43ce14b2d5..a54c0d13abe9f57af275737b051= dcb3b339e2975 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -32,6 +32,7 @@ TARGETS +=3D exec TARGETS +=3D fchmodat2 TARGETS +=3D filesystems TARGETS +=3D filesystems/binderfs +TARGETS +=3D filesystems/cuse TARGETS +=3D filesystems/epoll TARGETS +=3D filesystems/fat TARGETS +=3D filesystems/overlayfs diff --git a/tools/testing/selftests/filesystems/cuse/Makefile b/tools/test= ing/selftests/filesystems/cuse/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..4f0823f6bf93e89043fe85c2224= d324b8d144826 --- /dev/null +++ b/tools/testing/selftests/filesystems/cuse/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +CFLAGS +=3D -Wall -O2 -g $(KHDR_INCLUDES) + +TEST_GEN_PROGS :=3D cuse_leak_test + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/cuse/config b/tools/testin= g/selftests/filesystems/cuse/config new file mode 100644 index 0000000000000000000000000000000000000000..25d8c7c27496efd2918f85ed031= 4cbf111574413 --- /dev/null +++ b/tools/testing/selftests/filesystems/cuse/config @@ -0,0 +1,5 @@ +CONFIG_CUSE=3Dm +CONFIG_FAULT_INJECTION=3Dy +CONFIG_FAULT_INJECTION_DEBUG_FS=3Dy +CONFIG_FAILSLAB=3Dy +CONFIG_FAULT_INJECTION_STACKTRACE_FILTER=3Dy diff --git a/tools/testing/selftests/filesystems/cuse/cuse_leak_test.c b/to= ols/testing/selftests/filesystems/cuse/cuse_leak_test.c new file mode 100644 index 0000000000000000000000000000000000000000..a2d195da5896cb868dd1e6a9195= abf4dd75a2f97 --- /dev/null +++ b/tools/testing/selftests/filesystems/cuse/cuse_leak_test.c @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Regression test for CUSE device-node leak (missing device_del on + * error path). + * + * When cdev_alloc() fails after device_add() succeeds inside + * cuse_process_init_reply(), the error path must call device_del() + * before put_device(). Otherwise the /dev/ entry leaks and + * permanently poisons the device name. + * + * Test 1 (normal_cleanup): + * Completes a CUSE_INIT handshake, verifies the device appears, + * closes the channel fd, verifies the device is removed. + * + * Test 2 (leak_regression): + * Forces cdev_alloc() to fail via the cuse_inject_cdev_failure + * module parameter (preferred, requires CONFIG_FAULT_INJECTION in + * the CUSE build) or via failslab with stack-trace filtering + * (fallback). Verifies the /dev node is NOT leaked after close. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include "kselftest.h" + +#define CUSE_DEV "/dev/cuse" +#define BUF_SIZE (FUSE_MIN_READ_BUFFER + 4096) + +#define INJECT_PARAM "/sys/module/cuse/parameters/cuse_inject_cdev_failur= e" + +#define FAILSLAB "/sys/kernel/debug/failslab" +#define FAILSLAB_PROBABILITY FAILSLAB "/probability" +#define FAILSLAB_TIMES FAILSLAB "/times" +#define FAILSLAB_TASK_FILTER FAILSLAB "/task-filter" +#define FAILSLAB_STACK_DEPTH FAILSLAB "/stacktrace-depth" +#define FAILSLAB_REQUIRE_START FAILSLAB "/require-start" +#define FAILSLAB_REQUIRE_END FAILSLAB "/require-end" +#define MAKE_IT_FAIL "/proc/self/make-it-fail" + +static int write_file(const char *path, const char *val) +{ + int fd, len; + ssize_t n; + + fd =3D open(path, O_WRONLY); + if (fd < 0) + return -1; + len =3D strlen(val); + n =3D write(fd, val, len); + close(fd); + return n =3D=3D len ? 0 : -1; +} + +static int write_file_ul(const char *path, unsigned long val) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), "%lu", val); + return write_file(path, buf); +} + +static int read_file_int(const char *path) +{ + char buf[32]; + ssize_t n; + int fd; + + fd =3D open(path, O_RDONLY); + if (fd < 0) + return -1; + n =3D read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <=3D 0) + return -1; + buf[n] =3D '\0'; + return atoi(buf); +} + +static unsigned long kallsym_lookup(const char *name) +{ + unsigned long addr =3D 0; + char line[256]; + FILE *f; + + f =3D fopen("/proc/kallsyms", "r"); + if (!f) + return 0; + while (fgets(line, sizeof(line), f)) { + char sym[128]; + unsigned long a; + char type; + + if (sscanf(line, "%lx %c %127s", &a, &type, sym) =3D=3D 3 && + strcmp(sym, name) =3D=3D 0) { + addr =3D a; + break; + } + } + fclose(f); + return addr; +} + +static void failslab_cleanup(void) +{ + write_file(MAKE_IT_FAIL, "0"); + write_file(FAILSLAB_PROBABILITY, "0"); + write_file(FAILSLAB_TIMES, "0"); + write_file(FAILSLAB_TASK_FILTER, "0"); + write_file(FAILSLAB_REQUIRE_START, "0"); + write_file(FAILSLAB_REQUIRE_END, "0"); +} + +static int cuse_read_init(int fd, struct fuse_in_header *hdr_out) +{ + char buf[BUF_SIZE]; + struct fuse_in_header *hdr; + ssize_t n; + + n =3D read(fd, buf, sizeof(buf)); + if (n < (ssize_t)(sizeof(*hdr) + sizeof(struct cuse_init_in))) + return -1; + hdr =3D (struct fuse_in_header *)buf; + if (hdr->opcode !=3D CUSE_INIT) + return -1; + memcpy(hdr_out, hdr, sizeof(*hdr)); + return 0; +} + +static int cuse_send_init_reply(int fd, const struct fuse_in_header *hdr, + const char *devname) +{ + char reply[sizeof(struct fuse_out_header) + + sizeof(struct cuse_init_out) + 64]; + struct fuse_out_header *out_hdr; + struct cuse_init_out *init_out; + size_t info_len, reply_len; + char *info; + ssize_t n; + + memset(reply, 0, sizeof(reply)); + out_hdr =3D (struct fuse_out_header *)reply; + init_out =3D (struct cuse_init_out *)(reply + sizeof(*out_hdr)); + info =3D reply + sizeof(*out_hdr) + sizeof(*init_out); + + info_len =3D snprintf(info, 64, "DEVNAME=3D%s", devname) + 1; + reply_len =3D sizeof(*out_hdr) + sizeof(*init_out) + info_len; + + out_hdr->len =3D reply_len; + out_hdr->unique =3D hdr->unique; + init_out->major =3D FUSE_KERNEL_VERSION; + init_out->minor =3D FUSE_KERNEL_MINOR_VERSION; + init_out->flags =3D CUSE_UNRESTRICTED_IOCTL; + init_out->max_read =3D BUF_SIZE; + init_out->max_write =3D 4096; + + n =3D write(fd, reply, reply_len); + return n =3D=3D (ssize_t)reply_len ? 0 : -1; +} + +static int dev_exists(const char *devname) +{ + char path[128]; + + snprintf(path, sizeof(path), "/dev/%s", devname); + return access(path, F_OK) =3D=3D 0; +} + +/* + * Injection methods for forcing cdev_alloc() failure. + * + * METHOD_PARAM (preferred): cuse_inject_cdev_failure module parameter. + * Available when CUSE is built with CONFIG_FAULT_INJECTION. Directly + * forces cdev_alloc() to "fail" inside cuse_process_init_reply(). + * Works on all architectures including UML. + * + * METHOD_FAILSLAB (fallback): failslab with stack-trace filtering. + * Uses the kernel's generic slab fault injection to fail the kzalloc + * inside cdev_alloc(). Requires CONFIG_FAILSLAB and + * CONFIG_FAULT_INJECTION_STACKTRACE_FILTER. Depends on the stack + * unwinder producing matching addresses (may not work under UML). + */ +enum inject_method { + METHOD_NONE, + METHOD_PARAM, + METHOD_FAILSLAB, +}; + +static enum inject_method inject_available(void) +{ + if (access(INJECT_PARAM, W_OK) =3D=3D 0) + return METHOD_PARAM; + + if (access(FAILSLAB_REQUIRE_START, F_OK) =3D=3D 0 && + kallsym_lookup("cdev_alloc") !=3D 0) + return METHOD_FAILSLAB; + + return METHOD_NONE; +} + +static int inject_arm(enum inject_method m) +{ + unsigned long addr; + + switch (m) { + case METHOD_PARAM: + return write_file(INJECT_PARAM, "Y"); + + case METHOD_FAILSLAB: + addr =3D kallsym_lookup("cdev_alloc"); + if (!addr) + return -1; + write_file(FAILSLAB_TASK_FILTER, "1"); + write_file(FAILSLAB_STACK_DEPTH, "16"); + write_file_ul(FAILSLAB_REQUIRE_START, addr); + write_file_ul(FAILSLAB_REQUIRE_END, addr + 0x200); + write_file(FAILSLAB_TIMES, "1"); + write_file(FAILSLAB_PROBABILITY, "100"); + write_file(MAKE_IT_FAIL, "1"); + return 0; + + default: + return -1; + } +} + +static void inject_disarm(enum inject_method m) +{ + switch (m) { + case METHOD_PARAM: + write_file(INJECT_PARAM, "N"); + break; + case METHOD_FAILSLAB: + write_file(MAKE_IT_FAIL, "0"); + failslab_cleanup(); + break; + default: + break; + } +} + +static int inject_fired(enum inject_method m) +{ + switch (m) { + case METHOD_PARAM: + return 1; + case METHOD_FAILSLAB: + return read_file_int(FAILSLAB_TIMES) <=3D 0; + default: + return 0; + } +} + +static const char *inject_name(enum inject_method m) +{ + switch (m) { + case METHOD_PARAM: return "module parameter"; + case METHOD_FAILSLAB: return "failslab"; + default: return "none"; + } +} + +/* + * Test 1: normal CUSE init and cleanup. + * + * Verify a successfully initialized CUSE device is properly removed + * when the channel fd is closed (basic sanity). + */ +static void test_normal_cleanup(void) +{ + const char *name =3D "cuse_ksft_norm"; + struct fuse_in_header hdr; + int fd; + + fd =3D open(CUSE_DEV, O_RDWR | O_CLOEXEC); + if (fd < 0) { + ksft_test_result_skip("open %s: %s\n", CUSE_DEV, + strerror(errno)); + return; + } + + if (cuse_read_init(fd, &hdr)) { + close(fd); + ksft_test_result_fail("CUSE_INIT read failed\n"); + return; + } + + if (cuse_send_init_reply(fd, &hdr, name)) { + close(fd); + ksft_test_result_fail("CUSE_INIT reply write failed\n"); + return; + } + + usleep(100000); + if (!dev_exists(name)) { + close(fd); + ksft_test_result_fail("/dev/%s not created after init\n", name); + return; + } + + close(fd); + usleep(100000); + + if (dev_exists(name)) { + ksft_test_result_fail("/dev/%s not removed after close\n", + name); + return; + } + + ksft_test_result_pass("normal init and cleanup\n"); +} + +/* + * Test 2: regression test for device-node leak. + * + * Force cdev_alloc() to fail after device_add() has succeeded, then + * verify the /dev node is properly cleaned up by device_del(). + */ +static void test_leak_on_cdev_alloc_failure(void) +{ + const char *name =3D "cuse_ksft_leak"; + struct fuse_in_header hdr; + enum inject_method m; + int fd, fired; + + if (dev_exists(name)) { + ksft_test_result_skip("/dev/%s already exists (previous leak? reboot to = clear)\n", + name); + return; + } + + m =3D inject_available(); + if (m =3D=3D METHOD_NONE) { + ksft_test_result_skip( + "no injection method available (need CONFIG_FAULT_INJECTION)\n"); + return; + } + + fd =3D open(CUSE_DEV, O_RDWR | O_CLOEXEC); + if (fd < 0) { + ksft_test_result_skip("open %s: %s\n", CUSE_DEV, + strerror(errno)); + return; + } + + if (cuse_read_init(fd, &hdr)) { + close(fd); + ksft_test_result_fail("CUSE_INIT read failed\n"); + return; + } + + if (inject_arm(m)) { + close(fd); + ksft_test_result_fail("failed to arm injection (%s)\n", + inject_name(m)); + return; + } + + cuse_send_init_reply(fd, &hdr, name); + + fired =3D inject_fired(m); + inject_disarm(m); + + usleep(100000); + close(fd); + usleep(100000); + + if (!fired) { + ksft_test_result_skip("injection did not trigger (%s)\n", + inject_name(m)); + return; + } + + if (dev_exists(name)) { + ksft_test_result_fail( + "/dev/%s leaked: device_del() not called on cdev_alloc error path\n", + name); + return; + } + + ksft_test_result_pass("device node cleaned up after cdev_alloc failure (v= ia %s)\n", + inject_name(m)); +} + +int main(int argc, char **argv) +{ + ksft_print_header(); + ksft_set_plan(2); + + if (geteuid() !=3D 0) + ksft_exit_skip("must be run as root\n"); + + if (access(CUSE_DEV, F_OK) !=3D 0) + ksft_exit_skip(CUSE_DEV " not available (try: modprobe cuse)\n"); + + test_normal_cleanup(); + test_leak_on_cdev_alloc_failure(); + + ksft_finished(); +} --=20 2.52.0