Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH bpf-next v1 05/10] selftests/bpf: Add tests for KF_IMPLICIT_ARGS
From: Ihor Solodrai @ 2026-01-09 18:48 UTC (permalink / raw)
  To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman
  Cc: Mykyta Yatsenko, Tejun Heo, Alan Maguire, Benjamin Tissoires,
	Jiri Kosina, bpf, linux-kernel, linux-input, sched-ext
In-Reply-To: <20260109184852.1089786-1-ihor.solodrai@linux.dev>

Add trivial end-to-end tests to validate that KF_IMPLICIT_ARGS flag is
properly handled by both resolve_btfids and the verifier.

Declare kfuncs in bpf_testmod. Check that bpf_prog_aux pointer is set
in the kfunc implementation. Verify that calls with implicit args and
a legacy case all work.

Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
 .../bpf/prog_tests/kfunc_implicit_args.c      | 10 +++++
 .../selftests/bpf/progs/kfunc_implicit_args.c | 41 +++++++++++++++++++
 .../selftests/bpf/test_kmods/bpf_testmod.c    | 26 ++++++++++++
 3 files changed, 77 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/kfunc_implicit_args.c
 create mode 100644 tools/testing/selftests/bpf/progs/kfunc_implicit_args.c

diff --git a/tools/testing/selftests/bpf/prog_tests/kfunc_implicit_args.c b/tools/testing/selftests/bpf/prog_tests/kfunc_implicit_args.c
new file mode 100644
index 000000000000..5e4793c9c29a
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/kfunc_implicit_args.c
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <test_progs.h>
+#include "kfunc_implicit_args.skel.h"
+
+void test_kfunc_implicit_args(void)
+{
+	RUN_TESTS(kfunc_implicit_args);
+}
diff --git a/tools/testing/selftests/bpf/progs/kfunc_implicit_args.c b/tools/testing/selftests/bpf/progs/kfunc_implicit_args.c
new file mode 100644
index 000000000000..a1e456500442
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/kfunc_implicit_args.c
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+
+extern int bpf_kfunc_implicit_arg(int a) __weak __ksym;
+extern int bpf_kfunc_implicit_arg_impl(int a, void *aux__prog) __weak __ksym; // illegal
+extern int bpf_kfunc_implicit_arg_legacy(int a, int b) __weak __ksym;
+extern int bpf_kfunc_implicit_arg_legacy_impl(int a, int b, void *aux__prog) __weak __ksym;
+
+char _license[] SEC("license") = "GPL";
+
+SEC("syscall")
+__retval(5)
+int test_kfunc_implicit_arg(void *ctx)
+{
+	return bpf_kfunc_implicit_arg(5);
+}
+
+SEC("syscall")
+__failure
+int test_kfunc_implicit_arg_impl_illegal(void *ctx)
+{
+	return bpf_kfunc_implicit_arg_impl(5, NULL);
+}
+
+SEC("syscall")
+__retval(7)
+int test_kfunc_implicit_arg_legacy(void *ctx)
+{
+	return bpf_kfunc_implicit_arg_legacy(3, 4);
+}
+
+SEC("syscall")
+__retval(11)
+int test_kfunc_implicit_arg_legacy_impl(void *ctx)
+{
+	return bpf_kfunc_implicit_arg_legacy_impl(5, 6, NULL);
+}
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index 1c41d03bd5a1..503451875d33 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -1136,6 +1136,10 @@ __bpf_kfunc int bpf_kfunc_st_ops_inc10(struct st_ops_args *args)
 __bpf_kfunc int bpf_kfunc_multi_st_ops_test_1(struct st_ops_args *args, u32 id);
 __bpf_kfunc int bpf_kfunc_multi_st_ops_test_1_impl(struct st_ops_args *args, void *aux_prog);
 
+__bpf_kfunc int bpf_kfunc_implicit_arg(int a, struct bpf_prog_aux *aux);
+__bpf_kfunc int bpf_kfunc_implicit_arg_legacy(int a, int b, struct bpf_prog_aux *aux);
+__bpf_kfunc int bpf_kfunc_implicit_arg_legacy_impl(int a, int b, void *aux__prog);
+
 BTF_KFUNCS_START(bpf_testmod_check_kfunc_ids)
 BTF_ID_FLAGS(func, bpf_testmod_test_mod_kfunc)
 BTF_ID_FLAGS(func, bpf_kfunc_call_test1)
@@ -1178,6 +1182,9 @@ BTF_ID_FLAGS(func, bpf_kfunc_st_ops_test_pro_epilogue, KF_SLEEPABLE)
 BTF_ID_FLAGS(func, bpf_kfunc_st_ops_inc10)
 BTF_ID_FLAGS(func, bpf_kfunc_multi_st_ops_test_1)
 BTF_ID_FLAGS(func, bpf_kfunc_multi_st_ops_test_1_impl)
+BTF_ID_FLAGS(func, bpf_kfunc_implicit_arg, KF_IMPLICIT_ARGS)
+BTF_ID_FLAGS(func, bpf_kfunc_implicit_arg_legacy, KF_IMPLICIT_ARGS)
+BTF_ID_FLAGS(func, bpf_kfunc_implicit_arg_legacy_impl)
 BTF_KFUNCS_END(bpf_testmod_check_kfunc_ids)
 
 static int bpf_testmod_ops_init(struct btf *btf)
@@ -1669,6 +1676,25 @@ int bpf_kfunc_multi_st_ops_test_1_impl(struct st_ops_args *args, void *aux__prog
 	return ret;
 }
 
+int bpf_kfunc_implicit_arg(int a, struct bpf_prog_aux *aux)
+{
+	if (aux && a > 0)
+		return a;
+	return -EINVAL;
+}
+
+int bpf_kfunc_implicit_arg_legacy(int a, int b, struct bpf_prog_aux *aux)
+{
+	if (aux)
+		return a + b;
+	return -EINVAL;
+}
+
+int bpf_kfunc_implicit_arg_legacy_impl(int a, int b, void *aux__prog)
+{
+	return bpf_kfunc_implicit_arg_legacy(a, b, aux__prog);
+}
+
 static int multi_st_ops_reg(void *kdata, struct bpf_link *link)
 {
 	struct bpf_testmod_multi_st_ops *st_ops =
-- 
2.52.0


^ permalink raw reply related

* [PATCH bpf-next v1 04/10] resolve_btfids: Support for KF_IMPLICIT_ARGS
From: Ihor Solodrai @ 2026-01-09 18:48 UTC (permalink / raw)
  To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman
  Cc: Mykyta Yatsenko, Tejun Heo, Alan Maguire, Benjamin Tissoires,
	Jiri Kosina, bpf, linux-kernel, linux-input, sched-ext
In-Reply-To: <20260109184852.1089786-1-ihor.solodrai@linux.dev>

Implement BTF modifications in resolve_btfids to support BPF kernel
functions with implicit arguments.

For a kfunc marked with KF_IMPLICIT_ARGS flag, a new function
prototype is added to BTF that does not have implicit arguments. The
kfunc's prototype is then updated to a new one in BTF. This prototype
is the intended interface for the BPF programs.

A <func_name>_impl function is added to BTF to make the original kfunc
prototype searchable for the BPF verifier. If a <func_name>_impl
function already exists in BTF, its interpreted as a legacy case, and
this step is skipped.

Whether an argument is implicit is determined by its type:
currently only `struct bpf_prog_aux *` is supported.

As a result, the BTF associated with kfunc is changed from

    __bpf_kfunc bpf_foo(int arg1, struct bpf_prog_aux *aux);

into

    bpf_foo_impl(int arg1, struct bpf_prog_aux *aux);
    __bpf_kfunc bpf_foo(int arg1);

For more context see previous discussions and patches [1][2].

[1] https://lore.kernel.org/dwarves/ba1650aa-fafd-49a8-bea4-bdddee7c38c9@linux.dev/
[2] https://lore.kernel.org/bpf/20251029190113.3323406-1-ihor.solodrai@linux.dev/

Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
 tools/bpf/resolve_btfids/main.c | 282 ++++++++++++++++++++++++++++++++
 1 file changed, 282 insertions(+)

diff --git a/tools/bpf/resolve_btfids/main.c b/tools/bpf/resolve_btfids/main.c
index df39982f51df..b361e726fa36 100644
--- a/tools/bpf/resolve_btfids/main.c
+++ b/tools/bpf/resolve_btfids/main.c
@@ -152,6 +152,18 @@ struct object {
 	int nr_typedefs;
 };
 
+#define KF_IMPLICIT_ARGS (1 << 16)
+#define KF_IMPL_SUFFIX "_impl"
+#define MAX_BPF_FUNC_REG_ARGS 5
+#define MAX_KFUNCS 256
+#define MAX_DECL_TAGS (MAX_KFUNCS * 4)
+
+struct btf2btf_context {
+	struct btf *btf;
+	u32 nr_decl_tags;
+	s32 decl_tags[MAX_DECL_TAGS];
+};
+
 static int verbose;
 static int warnings;
 
@@ -972,6 +984,273 @@ static int patch_btfids(const char *btfids_path, const char *elf_path)
 	return err;
 }
 
+static s64 collect_kfunc_ids_by_flags(struct object *obj,
+				      u32 flags,
+				      s32 kfunc_ids[],
+				      const u32 kfunc_ids_sz)
+{
+	Elf_Data *data = obj->efile.idlist;
+	struct rb_node *next;
+	s64 nr_kfuncs = 0;
+	int i;
+
+	next = rb_first(&obj->sets);
+	while (next) {
+		struct btf_id_set8 *set8 = NULL;
+		unsigned long addr, off;
+		struct btf_id *id;
+
+		id = rb_entry(next, struct btf_id, rb_node);
+
+		if (id->kind != BTF_ID_KIND_SET8)
+			goto skip;
+
+		addr = id->addr[0];
+		off = addr - obj->efile.idlist_addr;
+		set8 = data->d_buf + off;
+
+		for (i = 0; i < set8->cnt; i++) {
+			if (set8->pairs[i].flags & flags) {
+				if (nr_kfuncs >= kfunc_ids_sz) {
+					pr_err("ERROR: resolve_btfids: too many kfuncs with flags %u - limit %d\n",
+					       flags, kfunc_ids_sz);
+					return -E2BIG;
+				}
+				kfunc_ids[nr_kfuncs++] = set8->pairs[i].id;
+			}
+		}
+skip:
+		next = rb_next(next);
+	}
+
+	return nr_kfuncs;
+}
+
+static const struct btf_type *btf__unqualified_type_by_id(const struct btf *btf, s32 type_id)
+{
+	const struct btf_type *t = btf__type_by_id(btf, type_id);
+
+	while (btf_is_mod(t))
+		t = btf__type_by_id(btf, t->type);
+
+	return t;
+}
+
+/* Implicit BPF kfunc arguments can only be of particular types */
+static bool btf__is_kf_implicit_arg(const struct btf *btf, const struct btf_param *p)
+{
+	static const char *const kf_implicit_arg_types[] = {
+		"bpf_prog_aux",
+	};
+	const struct btf_type *t;
+	const char *name;
+
+	t = btf__unqualified_type_by_id(btf, p->type);
+	if (!btf_is_ptr(t))
+		return false;
+
+	t = btf__unqualified_type_by_id(btf, t->type);
+	if (!btf_is_struct(t))
+		return false;
+
+	name = btf__name_by_offset(btf, t->name_off);
+	if (!name)
+		return false;
+
+	for (int i = 0; i < ARRAY_SIZE(kf_implicit_arg_types); i++)
+		if (strcmp(name, kf_implicit_arg_types[i]) == 0)
+			return true;
+
+	return false;
+}
+
+/*
+ * For a kfunc with KF_IMPLICIT_ARGS we do the following:
+ *   1. Add a new function with _impl suffix in the name, with the prototype
+ *      of the original kfunc.
+ *   2. Add all decl tags except "bpf_kfunc" for the _impl func.
+ *   3. Add a new function prototype with modified list of arguments:
+ *      omitting implicit args.
+ *   4. Change the prototype of the original kfunc to the new one.
+ *
+ * This way we transform the BTF associated with the kfunc from
+ *	__bpf_kfunc bpf_foo(int arg1, void *implicit_arg);
+ * into
+ *	bpf_foo_impl(int arg1, void *implicit_arg);
+ *	__bpf_kfunc bpf_foo(int arg1);
+ *
+ * If a kfunc with KF_IMPLICIT_ARGS already has an _impl counterpart
+ * in BTF, then it's a legacy case: an _impl function is declared in the
+ * source code. In this case, we can skip adding an _impl function, but we
+ * still have to add a func prototype that omits implicit args.
+ */
+static s64 process_kfunc_with_implicit_args(struct btf2btf_context *ctx, s32 kfunc_id)
+{
+	struct btf_param new_params[MAX_BPF_FUNC_REG_ARGS];
+	const char *kfunc_name, *param_name, *tag_name;
+	s32 proto_id, new_proto_id, new_func_id;
+	int err, len, name_len, nr_params;
+	const struct btf_param *params;
+	enum btf_func_linkage linkage;
+	char tmp_name[KSYM_NAME_LEN];
+	struct btf *btf = ctx->btf;
+	struct btf_type *t;
+
+	t = (struct btf_type *)btf__type_by_id(btf, kfunc_id);
+	if (!t || !btf_is_func(t)) {
+		pr_err("WARN: resolve_btfids: btf id %d is not a function\n", kfunc_id);
+		return -EINVAL;
+	}
+
+	kfunc_name = btf__name_by_offset(btf, t->name_off);
+	name_len = strlen(kfunc_name);
+	linkage = btf_vlen(t);
+
+	proto_id = t->type;
+	t = (struct btf_type *)btf__type_by_id(btf, proto_id);
+	if (!t || !btf_is_func_proto(t)) {
+		pr_err("ERROR: resolve_btfids: btf id %d is not a function prototype\n", proto_id);
+		return -EINVAL;
+	}
+
+	len = snprintf(tmp_name, sizeof(tmp_name), "%s%s", kfunc_name, KF_IMPL_SUFFIX);
+	if (len < 0 || len >= sizeof(tmp_name)) {
+		pr_err("ERROR: function name is too long: %s%s\n", kfunc_name, KF_IMPL_SUFFIX);
+		return -E2BIG;
+	}
+
+	if (btf__find_by_name_kind(btf, tmp_name, BTF_KIND_FUNC) > 0) {
+		pr_debug("resolve_btfids: function %s already exists in BTF\n", tmp_name);
+		goto add_new_proto;
+	}
+
+	/* Add a new function with _impl suffix and original prototype */
+	new_func_id = btf__add_func(btf, tmp_name, linkage, proto_id);
+	if (new_func_id < 0) {
+		pr_err("ERROR: resolve_btfids: failed to add func %s to BTF\n", tmp_name);
+		return new_func_id;
+	}
+
+	/* Copy all decl tags except "bpf_kfunc" from the original kfunc to the new one */
+	for (int i = 0; i < ctx->nr_decl_tags; i++) {
+		t = (struct btf_type *)btf__type_by_id(btf, ctx->decl_tags[i]);
+		if (t->type != kfunc_id)
+			continue;
+
+		tag_name = btf__name_by_offset(btf, t->name_off);
+		if (strcmp(tag_name, "bpf_kfunc") == 0)
+			continue;
+
+		err = btf__add_decl_tag(btf, tag_name, new_func_id, -1);
+		if (err < 0) {
+			pr_err("ERROR: resolve_btfids: failed to add decl tag %s for %s\n",
+			       tag_name, tmp_name);
+			return -EINVAL;
+		}
+	}
+
+add_new_proto:
+	/*
+	 * Drop the _impl suffix and point kfunc_name to the local buffer for later use.
+	 * When BTF is modified the original pointer is invalidated.
+	 */
+	tmp_name[name_len] = '\0';
+	kfunc_name = tmp_name;
+
+	/* Load non-implicit args from the original prototype */
+	t = (struct btf_type *)btf__type_by_id(btf, proto_id);
+	params = btf_params(t);
+	nr_params = 0;
+	for (int i = 0; i < btf_vlen(t); i++) {
+		if (btf__is_kf_implicit_arg(btf, &params[i]))
+			break;
+		new_params[nr_params++] = params[i];
+	}
+
+	new_proto_id = btf__add_func_proto(btf, t->type);
+	if (new_proto_id < 0) {
+		pr_err("ERROR: resolve_btfids: failed to add func proto for %s\n", kfunc_name);
+		return new_proto_id;
+	}
+
+	/* Add non-implicit args to the new prototype */
+	for (int i = 0; i < nr_params; i++) {
+		param_name = btf__name_by_offset(btf, new_params[i].name_off);
+		err = btf__add_func_param(btf, param_name, new_params[i].type);
+		if (err < 0) {
+			pr_err("ERROR: resolve_btfids: failed to add param %s for %s\n",
+			       param_name, kfunc_name);
+			return err;
+		}
+	}
+
+	/* Finally change the prototype of the original kfunc to the new one */
+	t = (struct btf_type *)btf__type_by_id(btf, kfunc_id);
+	t->type = new_proto_id;
+
+	pr_debug("resolve_btfids: updated BTF for kfunc with implicit args %s\n", kfunc_name);
+
+	return 0;
+}
+
+static s64 btf__collect_decl_tags(const struct btf *btf, s32 *decl_tags, u32 decl_tags_sz)
+{
+	const u32 type_cnt = btf__type_cnt(btf);
+	const struct btf_type *t;
+	s64 nr_decl_tags = 0;
+
+	for (u32 id = 1; id < type_cnt; id++) {
+		t = btf__type_by_id(btf, id);
+		if (!btf_is_decl_tag(t))
+			continue;
+		if (nr_decl_tags >= decl_tags_sz) {
+			pr_err("ERROR: resolve_btfids: too many decl tags in BTF - limit %s\n",
+				decl_tags_sz);
+			return -E2BIG;
+		}
+		decl_tags[nr_decl_tags++] = id;
+	}
+
+	return nr_decl_tags;
+}
+
+static s64 build_btf2btf_context(struct object *obj, struct btf2btf_context *ctx)
+{
+	s64 nr_decl_tags;
+
+	nr_decl_tags = btf__collect_decl_tags(obj->btf, ctx->decl_tags, ARRAY_SIZE(ctx->decl_tags));
+	if (nr_decl_tags < 0)
+		return nr_decl_tags;
+
+	ctx->btf = obj->btf;
+	ctx->nr_decl_tags = nr_decl_tags;
+
+	return 0;
+}
+
+static s64 finalize_btf(struct object *obj)
+{
+	struct btf2btf_context ctx = {};
+	s32 kfuncs[MAX_KFUNCS];
+	s64 err, nr_kfuncs;
+
+	err = build_btf2btf_context(obj, &ctx);
+	if (err < 0)
+		return err;
+
+	nr_kfuncs = collect_kfunc_ids_by_flags(obj, KF_IMPLICIT_ARGS, kfuncs, ARRAY_SIZE(kfuncs));
+	if (nr_kfuncs < 0)
+		return nr_kfuncs;
+
+	for (u32 i = 0; i < nr_kfuncs; i++) {
+		err = process_kfunc_with_implicit_args(&ctx, kfuncs[i]);
+		if (err < 0)
+			return err;
+	}
+
+	return 0;
+}
+
 static const char * const resolve_btfids_usage[] = {
 	"resolve_btfids [<options>] <ELF object>",
 	"resolve_btfids --patch_btfids <.BTF_ids file> <ELF object>",
@@ -1047,6 +1326,9 @@ int main(int argc, const char **argv)
 	if (symbols_patch(&obj))
 		goto out;
 
+	if (finalize_btf(&obj))
+		goto out;
+
 	err = make_out_path(out_path, sizeof(out_path), obj.path, BTF_IDS_SECTION);
 	err = err ?: dump_raw_btf_ids(&obj, out_path);
 	if (err)
-- 
2.52.0


^ permalink raw reply related

* [PATCH bpf-next v1 03/10] bpf: Verifier support for KF_IMPLICIT_ARGS
From: Ihor Solodrai @ 2026-01-09 18:48 UTC (permalink / raw)
  To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman
  Cc: Mykyta Yatsenko, Tejun Heo, Alan Maguire, Benjamin Tissoires,
	Jiri Kosina, bpf, linux-kernel, linux-input, sched-ext
In-Reply-To: <20260109184852.1089786-1-ihor.solodrai@linux.dev>

A kernel function bpf_foo marked with KF_IMPLICIT_ARGS flag is
expected to have two associated types in BTF:
  * `bpf_foo` with a function prototype that omits implicit arguments
  * `bpf_foo_impl` with a function prototype that matches the kernel
     declaration of `bpf_foo`, but doesn't have a ksym associated with
     its name

In order to support kfuncs with implicit arguments, the verifier has
to know how to resolve a call of `bpf_foo` to the correct BTF function
prototype and address.

To implement this, in add_kfunc_call() kfunc flags are checked for
KF_IMPLICIT_ARGS. For such kfuncs a BTF func prototype is adjusted to
the one found for `bpf_foo_impl` (func_name + "_impl" suffix, by
convention) function in BTF.

This effectively changes the signature of the `bpf_foo` kfunc in the
context of verification: from one without implicit args to the one
with full argument list.

Whether a kfunc argument is implicit or not is determined by
is_kfunc_arg_implicit(). The values of implicit arguments by design
are provided by the verifier, and so they can only be of particular
types. In this patch the only allowed implicit arg type is a pointer
to struct bpf_prog_aux. The __prog args (usually void *) are also
considered implicit for backwards compatibility.

In order to enable the verifier to correctly set an implicit
bpf_prog_aux arg value at runtime, is_kfunc_arg_prog() is extended to
check for the arg type. At a point when prog arg is determined in
check_kfunc_args() the kfunc with implicit args already has a
prototype with full argument list, so the existing value patch
mechanism just works.

If a new kfunc with KF_IMPLICIT_ARG is declared for an existing kfunc
that uses a __prog argument (a legacy case), the prototype
substitution works in exactly the same way, assuming the kfunc follows
the _impl naming convention. The difference is only in how _impl
prototype is added to the BTF, which is not the verifier's
concern. See a subsequent resolve_btfids patch for details.

In check_kfunc_call() reset the subreg_def of registers holding
implicit arguments to correctly track zero extensions.

Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
 include/linux/btf.h   |  1 +
 kernel/bpf/verifier.c | 70 +++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 69 insertions(+), 2 deletions(-)

diff --git a/include/linux/btf.h b/include/linux/btf.h
index bd261015c4bc..f64cc40ab96f 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -78,6 +78,7 @@
 #define KF_ARENA_RET    (1 << 13) /* kfunc returns an arena pointer */
 #define KF_ARENA_ARG1   (1 << 14) /* kfunc takes an arena pointer as its first argument */
 #define KF_ARENA_ARG2   (1 << 15) /* kfunc takes an arena pointer as its second argument */
+#define KF_IMPLICIT_ARGS (1 << 16) /* kfunc has implicit arguments supplied by the verifier */
 
 /*
  * Tag marking a kernel function as a kfunc. This is meant to minimize the
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 9618e4c37fce..5541ab674e30 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -3271,6 +3271,38 @@ static struct btf *find_kfunc_desc_btf(struct bpf_verifier_env *env, s16 offset)
 	return btf_vmlinux ?: ERR_PTR(-ENOENT);
 }
 
+#define KF_IMPL_SUFFIX "_impl"
+
+static const struct btf_type *find_kfunc_impl_proto(struct bpf_verifier_env *env,
+						    struct btf *btf,
+						    const char *func_name)
+{
+	char impl_name[KSYM_SYMBOL_LEN];
+	const struct btf_type *func;
+	s32 impl_id;
+	int len;
+
+	len = snprintf(impl_name, sizeof(impl_name), "%s%s", func_name, KF_IMPL_SUFFIX);
+	if (len < 0 || len >= sizeof(impl_name)) {
+		verbose(env, "function name %s%s is too long\n", func_name, KF_IMPL_SUFFIX);
+		return NULL;
+	}
+
+	impl_id = btf_find_by_name_kind(btf, impl_name, BTF_KIND_FUNC);
+	if (impl_id <= 0) {
+		verbose(env, "cannot find function %s in BTF\n", impl_name);
+		return NULL;
+	}
+
+	func = btf_type_by_id(btf, impl_id);
+	if (!func || !btf_type_is_func(func)) {
+		verbose(env, "%s (btf_id %d) is not a function\n", impl_name, impl_id);
+		return NULL;
+	}
+
+	return btf_type_by_id(btf, func->type);
+}
+
 static int fetch_kfunc_meta(struct bpf_verifier_env *env,
 			    s32 func_id,
 			    s16 offset,
@@ -3308,7 +3340,16 @@ static int fetch_kfunc_meta(struct bpf_verifier_env *env,
 	}
 
 	func_name = btf_name_by_offset(btf, func->name_off);
-	func_proto = btf_type_by_id(btf, func->type);
+
+	/*
+	 * An actual prototype of a kfunc with KF_IMPLICIT_ARGS flag
+	 * can be found through the counterpart _impl kfunc.
+	 */
+	if (unlikely(kfunc_flags && KF_IMPLICIT_ARGS & *kfunc_flags))
+		func_proto = find_kfunc_impl_proto(env, btf, func_name);
+	else
+		func_proto = btf_type_by_id(btf, func->type);
+
 	if (!func_proto || !btf_type_is_func_proto(func_proto)) {
 		verbose(env, "kernel function btf_id %d does not have a valid func_proto\n",
 			func_id);
@@ -12173,9 +12214,16 @@ static bool is_kfunc_arg_irq_flag(const struct btf *btf, const struct btf_param
 	return btf_param_match_suffix(btf, arg, "__irq_flag");
 }
 
+static bool is_kfunc_arg_prog_aux(const struct btf *btf, const struct btf_param *arg);
+
 static bool is_kfunc_arg_prog(const struct btf *btf, const struct btf_param *arg)
 {
-	return btf_param_match_suffix(btf, arg, "__prog");
+	return btf_param_match_suffix(btf, arg, "__prog") || is_kfunc_arg_prog_aux(btf, arg);
+}
+
+static bool is_kfunc_arg_implicit(const struct btf *btf, const struct btf_param *arg)
+{
+	return is_kfunc_arg_prog(btf, arg);
 }
 
 static bool is_kfunc_arg_scalar_with_name(const struct btf *btf,
@@ -12206,6 +12254,7 @@ enum {
 	KF_ARG_WORKQUEUE_ID,
 	KF_ARG_RES_SPIN_LOCK_ID,
 	KF_ARG_TASK_WORK_ID,
+	KF_ARG_PROG_AUX_ID
 };
 
 BTF_ID_LIST(kf_arg_btf_ids)
@@ -12217,6 +12266,7 @@ BTF_ID(struct, bpf_rb_node)
 BTF_ID(struct, bpf_wq)
 BTF_ID(struct, bpf_res_spin_lock)
 BTF_ID(struct, bpf_task_work)
+BTF_ID(struct, bpf_prog_aux)
 
 static bool __is_kfunc_ptr_arg_type(const struct btf *btf,
 				    const struct btf_param *arg, int type)
@@ -12297,6 +12347,11 @@ static bool is_kfunc_arg_callback(struct bpf_verifier_env *env, const struct btf
 	return true;
 }
 
+static bool is_kfunc_arg_prog_aux(const struct btf *btf, const struct btf_param *arg)
+{
+	return __is_kfunc_ptr_arg_type(btf, arg, KF_ARG_PROG_AUX_ID);
+}
+
 /* Returns true if struct is composed of scalars, 4 levels of nesting allowed */
 static bool __btf_type_is_scalar_struct(struct bpf_verifier_env *env,
 					const struct btf *btf,
@@ -14303,6 +14358,17 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
 	for (i = 0; i < nargs; i++) {
 		u32 regno = i + 1;
 
+		/*
+		 * Implicit kfunc arguments are set after main verification pass.
+		 * For correct tracking of zero-extensions we have to reset subreg_def for such
+		 * args. Otherwise mark_btf_func_reg_size() will be inspecting subreg_def of regs
+		 * from an earlier (irrelevant) point in the program, which may lead to an error
+		 * in opt_subreg_zext_lo32_rnd_hi32().
+		 */
+		if (unlikely(KF_IMPLICIT_ARGS & meta.kfunc_flags
+				&& is_kfunc_arg_implicit(desc_btf, &args[i])))
+			regs[regno].subreg_def = DEF_NOT_SUBREG;
+
 		t = btf_type_skip_modifiers(desc_btf, args[i].type, NULL);
 		if (btf_type_is_ptr(t))
 			mark_btf_func_reg_size(env, regno, sizeof(void *));
-- 
2.52.0


^ permalink raw reply related

* [PATCH bpf-next v1 00/10] bpf: Kernel functions with KF_IMPLICIT_ARGS
From: Ihor Solodrai @ 2026-01-09 18:48 UTC (permalink / raw)
  To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman
  Cc: Mykyta Yatsenko, Tejun Heo, Alan Maguire, Benjamin Tissoires,
	Jiri Kosina, bpf, linux-kernel, linux-input, sched-ext

This series implements a generic "implicit arguments" feature for BPF
kernel functions.

This is the next iteration of previous work [1][2], although it's not
really a v3, as parts of the implementation have changed
significantly.

A mechanism is created for kfuncs to have arguments that are not
visible to the BPF programs, and are provided to the kernel function
implementation by the verifier.

This mechanism is then used to define new APIs, that use
KF_IMPLICIT_ARGS flag, for the kfuncs that have a parameter with
__prog annotation [3], which is the current way of passing struct
bpf_prog_aux pointer to kfuncs.

The key difference in comparison to earlier work is that the necessary
BTF modifications are done by resolve_btfids (patch #4) instead of
pahole. The groundwork for this to be possible has been landed
recently [4].

This approach simplifies the implementation and future maintenance as
pahole is an out-of-tree tool, while resolve_btfids is not.

Another difference is in handling of the "legacy" case. This
implementation is more strict: the usage of <kfunc>_impl functions in
BPF is only allowed for kfuncs with an explicit kernel (or kmodule)
declaration (that is, existing _impl functions [5]). A <kfunc>_impl
function generated in BTF for a kfunc with implicit args does not have
a "bpf_kfunc" decl tag, and a kernel address. The verifier will reject
a program trying to call such an _impl kfunc.

The function with implicit arguments is defined by KF_IMPLICIT_ARGS
flag in BTF_IDS_FLAGS set. In this series, only a pointer to struct
bpf_prog_aux can be implicit, although it is simple to extend this to
more types.

The verifier handles a kfunc with KF_IMPLICIT_ARGS by resolving it to
a different (actual) BTF prototype early in verification (patch #3).

The series consists of the following patches:
  - patches #1 and #2 are non-functional refactoring in kernel/bpf
  - patch #3 defines KF_IMPLICIT_ARGS flag and teaches the verifier
    about it, enabling __prog args to also be implicit
  - patch #4 implements the necessary btf2btf transformation in
    resolve_btfids
  - patch #5 adds selftests specific to KF_IMPLICIT_ARGS feature
  - patches #6-#9 update the current users of __prog argument to use
    KF_IMPLICIT_ARGS
  - patch #10 updates relevant documentation

[1] https://lore.kernel.org/bpf/20251029190113.3323406-1-ihor.solodrai@linux.dev/
[2] https://lore.kernel.org/bpf/20250924211716.1287715-1-ihor.solodrai@linux.dev/
[3] https://docs.kernel.org/bpf/kfuncs.html#prog-annotation
[4] https://lore.kernel.org/bpf/20251219181321.1283664-1-ihor.solodrai@linux.dev/
[5] https://lore.kernel.org/bpf/20251104-implv2-v3-0-4772b9ae0e06@meta.com/

---

Ihor Solodrai (10):
  bpf: Refactor btf_kfunc_id_set_contains
  bpf: Introduce struct bpf_kfunc_meta
  bpf: Verifier support for KF_IMPLICIT_ARGS
  resolve_btfids: Support for KF_IMPLICIT_ARGS
  selftests/bpf: Add tests for KF_IMPLICIT_ARGS
  bpf: Add bpf_wq_set_callback kfunc with KF_IMPLICIT_ARGS
  HID: Use bpf_wq_set_callback kernel function
  bpf: Add bpf_task_work_schedule_* kfuncs with KF_IMPLICIT_ARGS
  bpf: Add bpf_stream_vprintk with KF_IMPLICIT_ARGS
  bpf,docs: Document KF_IMPLICIT_ARGS flag

 Documentation/bpf/kfuncs.rst                  |  44 ++-
 drivers/hid/bpf/progs/hid_bpf_helpers.h       |   8 +-
 include/linux/btf.h                           |   5 +-
 kernel/bpf/btf.c                              |  70 +++--
 kernel/bpf/helpers.c                          |  47 ++-
 kernel/bpf/stream.c                           |  11 +-
 kernel/bpf/verifier.c                         | 247 ++++++++++-----
 tools/bpf/resolve_btfids/main.c               | 282 ++++++++++++++++++
 tools/lib/bpf/bpf_helpers.h                   |   6 +-
 .../testing/selftests/bpf/bpf_experimental.h  |   5 -
 .../bpf/prog_tests/kfunc_implicit_args.c      |  10 +
 .../testing/selftests/bpf/progs/file_reader.c |   4 +-
 .../selftests/bpf/progs/kfunc_implicit_args.c |  41 +++
 .../testing/selftests/bpf/progs/stream_fail.c |   6 +-
 tools/testing/selftests/bpf/progs/task_work.c |  11 +-
 .../selftests/bpf/progs/task_work_fail.c      |  16 +-
 .../selftests/bpf/progs/task_work_stress.c    |   5 +-
 .../bpf/progs/verifier_async_cb_context.c     |   6 +-
 tools/testing/selftests/bpf/progs/wq.c        |   2 +-
 .../testing/selftests/bpf/progs/wq_failures.c |   4 +-
 .../selftests/bpf/test_kmods/bpf_testmod.c    |  26 ++
 tools/testing/selftests/hid/Makefile          |   4 +-
 .../selftests/hid/progs/hid_bpf_helpers.h     |   8 +-
 23 files changed, 724 insertions(+), 144 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/kfunc_implicit_args.c
 create mode 100644 tools/testing/selftests/bpf/progs/kfunc_implicit_args.c

-- 
2.52.0


^ permalink raw reply

* [PATCH bpf-next v1 02/10] bpf: Introduce struct bpf_kfunc_meta
From: Ihor Solodrai @ 2026-01-09 18:48 UTC (permalink / raw)
  To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman
  Cc: Mykyta Yatsenko, Tejun Heo, Alan Maguire, Benjamin Tissoires,
	Jiri Kosina, bpf, linux-kernel, linux-input, sched-ext
In-Reply-To: <20260109184852.1089786-1-ihor.solodrai@linux.dev>

There is code duplication between add_kfunc_call() and
fetch_kfunc_meta() collecting information about a kfunc from BTF.

Introduce struct bpf_kfunc_meta to hold common kfunc BTF data and
implement fetch_kfunc_meta() to fill it in, instead of struct
bpf_kfunc_call_arg_meta directly.

Then use these in add_kfunc_call() and (new) fetch_kfunc_arg_meta()
functions, and fixup previous usages of fetch_kfunc_meta() to
fetch_kfunc_arg_meta().

Besides the code dedup, this change enables add_kfunc_call() to access
kfunc->flags.

Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
 kernel/bpf/verifier.c | 156 ++++++++++++++++++++++++------------------
 1 file changed, 91 insertions(+), 65 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index bee49c9cde21..9618e4c37fce 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -294,6 +294,14 @@ struct bpf_call_arg_meta {
 	s64 const_map_key;
 };
 
+struct bpf_kfunc_meta {
+	struct btf *btf;
+	const struct btf_type *proto;
+	const char *name;
+	const u32 *flags;
+	s32 id;
+};
+
 struct bpf_kfunc_call_arg_meta {
 	/* In parameters */
 	struct btf *btf;
@@ -3263,16 +3271,68 @@ static struct btf *find_kfunc_desc_btf(struct bpf_verifier_env *env, s16 offset)
 	return btf_vmlinux ?: ERR_PTR(-ENOENT);
 }
 
-static int add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, s16 offset)
+static int fetch_kfunc_meta(struct bpf_verifier_env *env,
+			    s32 func_id,
+			    s16 offset,
+			    struct bpf_kfunc_meta *kfunc)
 {
 	const struct btf_type *func, *func_proto;
+	const char *func_name;
+	u32 *kfunc_flags;
+	struct btf *btf;
+
+	if (func_id <= 0) {
+		verbose(env, "invalid kernel function btf_id %d\n", func_id);
+		return -EINVAL;
+	}
+
+	btf = find_kfunc_desc_btf(env, offset);
+	if (IS_ERR(btf)) {
+		verbose(env, "failed to find BTF for kernel function\n");
+		return PTR_ERR(btf);
+	}
+
+	/*
+	 * Note that kfunc_flags may be NULL at this point, which
+	 * means that we couldn't find func_id in any relevant
+	 * kfunc_id_set. This most likely indicates an invalid kfunc
+	 * call.  However we don't fail with an error here,
+	 * and let the caller decide what to do with NULL kfunc->flags.
+	 */
+	kfunc_flags = btf_kfunc_flags(btf, func_id, env->prog);
+
+	func = btf_type_by_id(btf, func_id);
+	if (!func || !btf_type_is_func(func)) {
+		verbose(env, "kernel btf_id %d is not a function\n", func_id);
+		return -EINVAL;
+	}
+
+	func_name = btf_name_by_offset(btf, func->name_off);
+	func_proto = btf_type_by_id(btf, func->type);
+	if (!func_proto || !btf_type_is_func_proto(func_proto)) {
+		verbose(env, "kernel function btf_id %d does not have a valid func_proto\n",
+			func_id);
+		return -EINVAL;
+	}
+
+	memset(kfunc, 0, sizeof(*kfunc));
+	kfunc->btf = btf;
+	kfunc->id = func_id;
+	kfunc->name = func_name;
+	kfunc->proto = func_proto;
+	kfunc->flags = kfunc_flags;
+
+	return 0;
+}
+
+static int add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, s16 offset)
+{
 	struct bpf_kfunc_btf_tab *btf_tab;
 	struct btf_func_model func_model;
 	struct bpf_kfunc_desc_tab *tab;
 	struct bpf_prog_aux *prog_aux;
+	struct bpf_kfunc_meta kfunc;
 	struct bpf_kfunc_desc *desc;
-	const char *func_name;
-	struct btf *desc_btf;
 	unsigned long addr;
 	int err;
 
@@ -3322,12 +3382,6 @@ static int add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, s16 offset)
 		prog_aux->kfunc_btf_tab = btf_tab;
 	}
 
-	desc_btf = find_kfunc_desc_btf(env, offset);
-	if (IS_ERR(desc_btf)) {
-		verbose(env, "failed to find BTF for kernel function\n");
-		return PTR_ERR(desc_btf);
-	}
-
 	if (find_kfunc_desc(env->prog, func_id, offset))
 		return 0;
 
@@ -3336,24 +3390,13 @@ static int add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, s16 offset)
 		return -E2BIG;
 	}
 
-	func = btf_type_by_id(desc_btf, func_id);
-	if (!func || !btf_type_is_func(func)) {
-		verbose(env, "kernel btf_id %u is not a function\n",
-			func_id);
-		return -EINVAL;
-	}
-	func_proto = btf_type_by_id(desc_btf, func->type);
-	if (!func_proto || !btf_type_is_func_proto(func_proto)) {
-		verbose(env, "kernel function btf_id %u does not have a valid func_proto\n",
-			func_id);
-		return -EINVAL;
-	}
+	err = fetch_kfunc_meta(env, func_id, offset, &kfunc);
+	if (err)
+		return err;
 
-	func_name = btf_name_by_offset(desc_btf, func->name_off);
-	addr = kallsyms_lookup_name(func_name);
+	addr = kallsyms_lookup_name(kfunc.name);
 	if (!addr) {
-		verbose(env, "cannot find address for kernel function %s\n",
-			func_name);
+		verbose(env, "cannot find address for kernel function %s\n", kfunc.name);
 		return -EINVAL;
 	}
 
@@ -3363,9 +3406,7 @@ static int add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, s16 offset)
 			return err;
 	}
 
-	err = btf_distill_func_proto(&env->log, desc_btf,
-				     func_proto, func_name,
-				     &func_model);
+	err = btf_distill_func_proto(&env->log, kfunc.btf, kfunc.proto, kfunc.name, &func_model);
 	if (err)
 		return err;
 
@@ -13695,44 +13736,28 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
 	return 0;
 }
 
-static int fetch_kfunc_meta(struct bpf_verifier_env *env,
-			    struct bpf_insn *insn,
-			    struct bpf_kfunc_call_arg_meta *meta,
-			    const char **kfunc_name)
+static int fetch_kfunc_arg_meta(struct bpf_verifier_env *env,
+				s32 func_id,
+				s16 offset,
+				struct bpf_kfunc_call_arg_meta *meta)
 {
-	const struct btf_type *func, *func_proto;
-	u32 func_id, *kfunc_flags;
-	const char *func_name;
-	struct btf *desc_btf;
-
-	if (kfunc_name)
-		*kfunc_name = NULL;
-
-	if (!insn->imm)
-		return -EINVAL;
+	struct bpf_kfunc_meta kfunc;
+	int err;
 
-	desc_btf = find_kfunc_desc_btf(env, insn->off);
-	if (IS_ERR(desc_btf))
-		return PTR_ERR(desc_btf);
+	err = fetch_kfunc_meta(env, func_id, offset, &kfunc);
+	if (err)
+		return err;
 
-	func_id = insn->imm;
-	func = btf_type_by_id(desc_btf, func_id);
-	func_name = btf_name_by_offset(desc_btf, func->name_off);
-	if (kfunc_name)
-		*kfunc_name = func_name;
-	func_proto = btf_type_by_id(desc_btf, func->type);
+	memset(meta, 0, sizeof(*meta));
+	meta->btf = kfunc.btf;
+	meta->func_id = kfunc.id;
+	meta->func_proto = kfunc.proto;
+	meta->func_name = kfunc.name;
 
-	if (!btf_kfunc_is_allowed(desc_btf, func_id, env->prog))
+	if (!kfunc.flags || !btf_kfunc_is_allowed(kfunc.btf, kfunc.id, env->prog))
 		return -EACCES;
 
-	kfunc_flags = btf_kfunc_flags(desc_btf, func_id, env->prog);
-
-	memset(meta, 0, sizeof(*meta));
-	meta->btf = desc_btf;
-	meta->func_id = func_id;
-	meta->kfunc_flags = *kfunc_flags;
-	meta->func_proto = func_proto;
-	meta->func_name = func_name;
+	meta->kfunc_flags = *kfunc.flags;
 
 	return 0;
 }
@@ -13937,12 +13962,13 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
 	if (!insn->imm)
 		return 0;
 
-	err = fetch_kfunc_meta(env, insn, &meta, &func_name);
-	if (err == -EACCES && func_name)
-		verbose(env, "calling kernel function %s is not allowed\n", func_name);
+	err = fetch_kfunc_arg_meta(env, insn->imm, insn->off, &meta);
+	if (err == -EACCES && meta.func_name)
+		verbose(env, "calling kernel function %s is not allowed\n", meta.func_name);
 	if (err)
 		return err;
 	desc_btf = meta.btf;
+	func_name = meta.func_name;
 	insn_aux = &env->insn_aux_data[insn_idx];
 
 	insn_aux->is_iter_next = is_iter_next_kfunc(&meta);
@@ -17729,7 +17755,7 @@ static bool get_call_summary(struct bpf_verifier_env *env, struct bpf_insn *call
 	if (bpf_pseudo_kfunc_call(call)) {
 		int err;
 
-		err = fetch_kfunc_meta(env, call, &meta, NULL);
+		err = fetch_kfunc_arg_meta(env, call->imm, call->off, &meta);
 		if (err < 0)
 			/* error would be reported later */
 			return false;
@@ -18237,7 +18263,7 @@ static int visit_insn(int t, struct bpf_verifier_env *env)
 		} else if (insn->src_reg == BPF_PSEUDO_KFUNC_CALL) {
 			struct bpf_kfunc_call_arg_meta meta;
 
-			ret = fetch_kfunc_meta(env, insn, &meta, NULL);
+			ret = fetch_kfunc_arg_meta(env, insn->imm, insn->off, &meta);
 			if (ret == 0 && is_iter_next_kfunc(&meta)) {
 				mark_prune_point(env, t);
 				/* Checking and saving state checkpoints at iter_next() call
-- 
2.52.0


^ permalink raw reply related

* [PATCH bpf-next v1 01/10] bpf: Refactor btf_kfunc_id_set_contains
From: Ihor Solodrai @ 2026-01-09 18:48 UTC (permalink / raw)
  To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman
  Cc: Mykyta Yatsenko, Tejun Heo, Alan Maguire, Benjamin Tissoires,
	Jiri Kosina, bpf, linux-kernel, linux-input, sched-ext
In-Reply-To: <20260109184852.1089786-1-ihor.solodrai@linux.dev>

btf_kfunc_id_set_contains() is called by fetch_kfunc_meta() in the BPF
verifier to get the kfunc flags stored in the .BTF_ids ELF section.
If it returns NULL instead of a valid pointer, it's interpreted as an
illegal kfunc usage failing the verification.

There are two potential reasons for btf_kfunc_id_set_contains() to
return NULL:

  1. Provided kfunc BTF id is not present in relevant kfunc id sets.
  2. The kfunc is not allowed, as determined by the program type
     specific filter [1].

The filter functions accept a pointer to `struct bpf_prog`, so they
might implicitly depend on earlier stages of verification, when
bpf_prog members are set.

For example, bpf_qdisc_kfunc_filter() in linux/net/sched/bpf_qdisc.c
inspects prog->aux->st_ops [2], which is initialized in:

    check_attach_btf_id() -> check_struct_ops_btf_id()

So far this hasn't been an issue, because fetch_kfunc_meta() is the
only caller of btf_kfunc_id_set_contains().

However in subsequent patches of this series it is necessary to
inspect kfunc flags earlier in BPF verifier, in the add_kfunc_call().

To resolve this, refactor btf_kfunc_id_set_contains() into two
interface functions:
  * btf_kfunc_flags() that simply returns pointer to kfunc_flags
    without applying the filters
  * btf_kfunc_is_allowed() that both checks for kfunc_flags existence
    (which is a requirement for a kfunc to be allowed) and applies the
    prog filters

See [3] for the previous version of this patch.

[1] https://lore.kernel.org/all/20230519225157.760788-7-aditi.ghag@isovalent.com/
[2] https://lore.kernel.org/all/20250409214606.2000194-4-ameryhung@gmail.com/
[3] https://lore.kernel.org/bpf/20251029190113.3323406-3-ihor.solodrai@linux.dev/

Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
 include/linux/btf.h   |  4 +--
 kernel/bpf/btf.c      | 70 ++++++++++++++++++++++++++++++++-----------
 kernel/bpf/verifier.c |  6 ++--
 3 files changed, 58 insertions(+), 22 deletions(-)

diff --git a/include/linux/btf.h b/include/linux/btf.h
index 691f09784933..bd261015c4bc 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -574,8 +574,8 @@ const char *btf_name_by_offset(const struct btf *btf, u32 offset);
 const char *btf_str_by_offset(const struct btf *btf, u32 offset);
 struct btf *btf_parse_vmlinux(void);
 struct btf *bpf_prog_get_target_btf(const struct bpf_prog *prog);
-u32 *btf_kfunc_id_set_contains(const struct btf *btf, u32 kfunc_btf_id,
-			       const struct bpf_prog *prog);
+u32 *btf_kfunc_flags(const struct btf *btf, u32 kfunc_btf_id, const struct bpf_prog *prog);
+bool btf_kfunc_is_allowed(const struct btf *btf, u32 kfunc_btf_id, const struct bpf_prog *prog);
 u32 *btf_kfunc_is_modify_return(const struct btf *btf, u32 kfunc_btf_id,
 				const struct bpf_prog *prog);
 int register_btf_kfunc_id_set(enum bpf_prog_type prog_type,
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 539c9fdea41d..32974c11664d 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -8640,24 +8640,17 @@ static int btf_populate_kfunc_set(struct btf *btf, enum btf_kfunc_hook hook,
 	return ret;
 }
 
-static u32 *__btf_kfunc_id_set_contains(const struct btf *btf,
-					enum btf_kfunc_hook hook,
-					u32 kfunc_btf_id,
-					const struct bpf_prog *prog)
+static u32 *btf_kfunc_id_set_contains(const struct btf *btf,
+				      enum btf_kfunc_hook hook,
+				      u32 kfunc_btf_id)
 {
-	struct btf_kfunc_hook_filter *hook_filter;
 	struct btf_id_set8 *set;
-	u32 *id, i;
+	u32 *id;
 
 	if (hook >= BTF_KFUNC_HOOK_MAX)
 		return NULL;
 	if (!btf->kfunc_set_tab)
 		return NULL;
-	hook_filter = &btf->kfunc_set_tab->hook_filters[hook];
-	for (i = 0; i < hook_filter->nr_filters; i++) {
-		if (hook_filter->filters[i](prog, kfunc_btf_id))
-			return NULL;
-	}
 	set = btf->kfunc_set_tab->sets[hook];
 	if (!set)
 		return NULL;
@@ -8668,6 +8661,28 @@ static u32 *__btf_kfunc_id_set_contains(const struct btf *btf,
 	return id + 1;
 }
 
+static bool __btf_kfunc_is_allowed(const struct btf *btf,
+				   enum btf_kfunc_hook hook,
+				   u32 kfunc_btf_id,
+				   const struct bpf_prog *prog)
+{
+	struct btf_kfunc_hook_filter *hook_filter;
+	int i;
+
+	if (hook >= BTF_KFUNC_HOOK_MAX)
+		return false;
+	if (!btf->kfunc_set_tab)
+		return false;
+
+	hook_filter = &btf->kfunc_set_tab->hook_filters[hook];
+	for (i = 0; i < hook_filter->nr_filters; i++) {
+		if (hook_filter->filters[i](prog, kfunc_btf_id))
+			return false;
+	}
+
+	return true;
+}
+
 static int bpf_prog_type_to_kfunc_hook(enum bpf_prog_type prog_type)
 {
 	switch (prog_type) {
@@ -8715,6 +8730,26 @@ static int bpf_prog_type_to_kfunc_hook(enum bpf_prog_type prog_type)
 	}
 }
 
+bool btf_kfunc_is_allowed(const struct btf *btf,
+			  u32 kfunc_btf_id,
+			  const struct bpf_prog *prog)
+{
+	enum bpf_prog_type prog_type = resolve_prog_type(prog);
+	enum btf_kfunc_hook hook;
+	u32 *kfunc_flags;
+
+	kfunc_flags = btf_kfunc_id_set_contains(btf, BTF_KFUNC_HOOK_COMMON, kfunc_btf_id);
+	if (kfunc_flags && __btf_kfunc_is_allowed(btf, BTF_KFUNC_HOOK_COMMON, kfunc_btf_id, prog))
+		return true;
+
+	hook = bpf_prog_type_to_kfunc_hook(prog_type);
+	kfunc_flags = btf_kfunc_id_set_contains(btf, hook, kfunc_btf_id);
+	if (kfunc_flags && __btf_kfunc_is_allowed(btf, hook, kfunc_btf_id, prog))
+		return true;
+
+	return false;
+}
+
 /* Caution:
  * Reference to the module (obtained using btf_try_get_module) corresponding to
  * the struct btf *MUST* be held when calling this function from verifier
@@ -8722,26 +8757,27 @@ static int bpf_prog_type_to_kfunc_hook(enum bpf_prog_type prog_type)
  * keeping the reference for the duration of the call provides the necessary
  * protection for looking up a well-formed btf->kfunc_set_tab.
  */
-u32 *btf_kfunc_id_set_contains(const struct btf *btf,
-			       u32 kfunc_btf_id,
-			       const struct bpf_prog *prog)
+u32 *btf_kfunc_flags(const struct btf *btf, u32 kfunc_btf_id, const struct bpf_prog *prog)
 {
 	enum bpf_prog_type prog_type = resolve_prog_type(prog);
 	enum btf_kfunc_hook hook;
 	u32 *kfunc_flags;
 
-	kfunc_flags = __btf_kfunc_id_set_contains(btf, BTF_KFUNC_HOOK_COMMON, kfunc_btf_id, prog);
+	kfunc_flags = btf_kfunc_id_set_contains(btf, BTF_KFUNC_HOOK_COMMON, kfunc_btf_id);
 	if (kfunc_flags)
 		return kfunc_flags;
 
 	hook = bpf_prog_type_to_kfunc_hook(prog_type);
-	return __btf_kfunc_id_set_contains(btf, hook, kfunc_btf_id, prog);
+	return btf_kfunc_id_set_contains(btf, hook, kfunc_btf_id);
 }
 
 u32 *btf_kfunc_is_modify_return(const struct btf *btf, u32 kfunc_btf_id,
 				const struct bpf_prog *prog)
 {
-	return __btf_kfunc_id_set_contains(btf, BTF_KFUNC_HOOK_FMODRET, kfunc_btf_id, prog);
+	if (!__btf_kfunc_is_allowed(btf, BTF_KFUNC_HOOK_FMODRET, kfunc_btf_id, prog))
+		return NULL;
+
+	return btf_kfunc_id_set_contains(btf, BTF_KFUNC_HOOK_FMODRET, kfunc_btf_id);
 }
 
 static int __register_btf_kfunc_id_set(enum btf_kfunc_hook hook,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 53635ea2e41b..bee49c9cde21 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -13722,10 +13722,10 @@ static int fetch_kfunc_meta(struct bpf_verifier_env *env,
 		*kfunc_name = func_name;
 	func_proto = btf_type_by_id(desc_btf, func->type);
 
-	kfunc_flags = btf_kfunc_id_set_contains(desc_btf, func_id, env->prog);
-	if (!kfunc_flags) {
+	if (!btf_kfunc_is_allowed(desc_btf, func_id, env->prog))
 		return -EACCES;
-	}
+
+	kfunc_flags = btf_kfunc_flags(desc_btf, func_id, env->prog);
 
 	memset(meta, 0, sizeof(*meta));
 	meta->btf = desc_btf;
-- 
2.52.0


^ permalink raw reply related

* Re: [PATCH v2] HID: logitech-hidpp: Check maxfield in hidpp_get_report_length()
From: Greg KH @ 2026-01-09 13:00 UTC (permalink / raw)
  To: Günther Noack
  Cc: Filipe Laíns, Bastien Nocera, Jiri Kosina,
	Benjamin Tissoires, stable, linux-input, linux-kernel
In-Reply-To: <20260109122557.3166556-3-gnoack@google.com>

On Fri, Jan 09, 2026 at 01:25:58PM +0100, Günther Noack wrote:
> Do not crash when a report has no fields.
> 
> Fake USB gadgets can send their own HID report descriptors and can define report
> structures without valid fields.  This can be used to crash the kernel over USB.
> 
> Cc: stable@vger.kernel.org
> Signed-off-by: Günther Noack <gnoack@google.com>
> ---
>  drivers/hid/hid-logitech-hidpp.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> index 9ced0e4d46ae..c5e132a6c085 100644
> --- a/drivers/hid/hid-logitech-hidpp.c
> +++ b/drivers/hid/hid-logitech-hidpp.c
> @@ -4313,7 +4313,7 @@ static int hidpp_get_report_length(struct hid_device *hdev, int id)
>  
>  	re = &(hdev->report_enum[HID_OUTPUT_REPORT]);
>  	report = re->report_id_hash[id];
> -	if (!report)
> +	if (!report || !report->maxfield)
>  		return 0;
>  
>  	return report->field[0]->report_count + 1;
> -- 
> 2.52.0.457.g6b5491de43-goog
> 


Hi,

This is the friendly patch-bot of Greg Kroah-Hartman.  You have sent him
a patch that has triggered this response.  He used to manually respond
to these common problems, but in order to save his sanity (he kept
writing the same thing over and over, yet to different people), I was
created.  Hopefully you will not take offence and will fix the problem
in your patch and resubmit it so that it can be accepted into the Linux
kernel tree.

You are receiving this message because of the following common error(s)
as indicated below:

- This looks like a new version of a previously submitted patch, but you
  did not list below the --- line any changes from the previous version.
  Please read the section entitled "The canonical patch format" in the
  kernel file, Documentation/process/submitting-patches.rst for what
  needs to be done here to properly describe this.

If you wish to discuss this problem further, or you have questions about
how to resolve this issue, please feel free to respond to this email and
Greg will reply once he has dug out from the pending patches received
from other developers.

thanks,

greg k-h's patch email bot

^ permalink raw reply

* Re: [PATCH] HID: logitech-hidpp: Check maxfield in hidpp_get_report_length()
From: Günther Noack @ 2026-01-09 12:41 UTC (permalink / raw)
  To: Greg KH
  Cc: Filipe Laíns, Bastien Nocera, Jiri Kosina,
	Benjamin Tissoires, stable, linux-input, linux-kernel
In-Reply-To: <2026010956-anteater-pungent-d5b6@gregkh>

On Fri, Jan 09, 2026 at 12:14:43PM +0100, Greg KH wrote:
> On Fri, Jan 09, 2026 at 11:59:12AM +0100, Günther Noack wrote:
> > Do not crash when a report has no fields.
> > 
> > Fake USB gadgets can send their own HID report descriptors and can define report
> > structures without valid fields.  This can be used to crash the kernel over USB.
> > 
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Günther Noack <gnoack@google.com>
> > ---
> >  drivers/hid/hid-logitech-hidpp.c | 3 +++
> >  1 file changed, 3 insertions(+)
> > 
> > diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> > index 9ced0e4d46ae..919ba9f50292 100644
> > --- a/drivers/hid/hid-logitech-hidpp.c
> > +++ b/drivers/hid/hid-logitech-hidpp.c
> > @@ -4316,6 +4316,9 @@ static int hidpp_get_report_length(struct hid_device *hdev, int id)
> >  	if (!report)
> >  		return 0;
> >  
> > +	if (!report->maxfield)
> > +		return 0;
> 
> Combine this with the if() above this?

OK, done. I sent a V2:
https://lore.kernel.org/all/20260109122557.3166556-3-gnoack@google.com/


> And if we are going to be handling "malicious" USB devices, be careful,
> you are just moving the target lower down, you also need to audit ALL
> data coming from the device, not just the descriptors.  I'm all for
> this, just realize that this is a change in how Linux treats devices
> (and all other operating systems as well.)

Thanks.  Yes, I realize that the later communication with the device is also a
potential way to trigger bugs.


> For now, we strongly recommend not allowing "untrusted" devices to bind
> to your system if this is a threat model you care about.
> 
> Not to reject this, or your other patch like this, just letting you
> know.

Acknowledged, thanks.

-Günther

^ permalink raw reply

* [PATCH v2] HID: logitech-hidpp: Check maxfield in hidpp_get_report_length()
From: Günther Noack @ 2026-01-09 12:25 UTC (permalink / raw)
  To: Filipe Laíns, Bastien Nocera, Jiri Kosina,
	Benjamin Tissoires
  Cc: Greg KH, Günther Noack, stable, linux-input, linux-kernel

Do not crash when a report has no fields.

Fake USB gadgets can send their own HID report descriptors and can define report
structures without valid fields.  This can be used to crash the kernel over USB.

Cc: stable@vger.kernel.org
Signed-off-by: Günther Noack <gnoack@google.com>
---
 drivers/hid/hid-logitech-hidpp.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 9ced0e4d46ae..c5e132a6c085 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -4313,7 +4313,7 @@ static int hidpp_get_report_length(struct hid_device *hdev, int id)
 
 	re = &(hdev->report_enum[HID_OUTPUT_REPORT]);
 	report = re->report_id_hash[id];
-	if (!report)
+	if (!report || !report->maxfield)
 		return 0;
 
 	return report->field[0]->report_count + 1;
-- 
2.52.0.457.g6b5491de43-goog


^ permalink raw reply related

* Re: [PATCH] HID: logitech-hidpp: Check maxfield in hidpp_get_report_length()
From: Greg KH @ 2026-01-09 11:14 UTC (permalink / raw)
  To: Günther Noack
  Cc: Filipe Laíns, Bastien Nocera, Jiri Kosina,
	Benjamin Tissoires, stable, linux-input, linux-kernel
In-Reply-To: <20260109105912.3141960-2-gnoack@google.com>

On Fri, Jan 09, 2026 at 11:59:12AM +0100, Günther Noack wrote:
> Do not crash when a report has no fields.
> 
> Fake USB gadgets can send their own HID report descriptors and can define report
> structures without valid fields.  This can be used to crash the kernel over USB.
> 
> Cc: stable@vger.kernel.org
> Signed-off-by: Günther Noack <gnoack@google.com>
> ---
>  drivers/hid/hid-logitech-hidpp.c | 3 +++
>  1 file changed, 3 insertions(+)
> 
> diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> index 9ced0e4d46ae..919ba9f50292 100644
> --- a/drivers/hid/hid-logitech-hidpp.c
> +++ b/drivers/hid/hid-logitech-hidpp.c
> @@ -4316,6 +4316,9 @@ static int hidpp_get_report_length(struct hid_device *hdev, int id)
>  	if (!report)
>  		return 0;
>  
> +	if (!report->maxfield)
> +		return 0;

Combine this with the if() above this?

And if we are going to be handling "malicious" USB devices, be careful,
you are just moving the target lower down, you also need to audit ALL
data coming from the device, not just the descriptors.  I'm all for
this, just realize that this is a change in how Linux treats devices
(and all other operating systems as well.)

For now, we strongly recommend not allowing "untrusted" devices to bind
to your system if this is a threat model you care about.

Not to reject this, or your other patch like this, just letting you
know.

thanks,

greg k-h

^ permalink raw reply

* [PATCH] HID: logitech-hidpp: Check maxfield in hidpp_get_report_length()
From: Günther Noack @ 2026-01-09 10:59 UTC (permalink / raw)
  To: Filipe Laíns, Bastien Nocera, Jiri Kosina,
	Benjamin Tissoires
  Cc: Günther Noack, stable, linux-input, linux-kernel

Do not crash when a report has no fields.

Fake USB gadgets can send their own HID report descriptors and can define report
structures without valid fields.  This can be used to crash the kernel over USB.

Cc: stable@vger.kernel.org
Signed-off-by: Günther Noack <gnoack@google.com>
---
 drivers/hid/hid-logitech-hidpp.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 9ced0e4d46ae..919ba9f50292 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -4316,6 +4316,9 @@ static int hidpp_get_report_length(struct hid_device *hdev, int id)
 	if (!report)
 		return 0;
 
+	if (!report->maxfield)
+		return 0;
+
 	return report->field[0]->report_count + 1;
 }
 
-- 
2.52.0.457.g6b5491de43-goog


^ permalink raw reply related

* [PATCH] HID: prodikeys: Check presence of pm->input_ep82
From: Günther Noack @ 2026-01-09 10:58 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Günther Noack, stable, linux-input, linux-kernel

Fake USB devices can send their own report descriptors for which the
input_mapping() hook does not get called.  In this case, pm->input_ep82 stays
NULL, which leads to a crash later.

This does not happen with the real device, but can be provoked by imposing as
one.

Cc: stable@vger.kernel.org
Signed-off-by: Günther Noack <gnoack@google.com>
---
 drivers/hid/hid-prodikeys.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/drivers/hid/hid-prodikeys.c b/drivers/hid/hid-prodikeys.c
index 74bddb2c3e82..6e413df38358 100644
--- a/drivers/hid/hid-prodikeys.c
+++ b/drivers/hid/hid-prodikeys.c
@@ -378,6 +378,10 @@ static int pcmidi_handle_report4(struct pcmidi_snd *pm, u8 *data)
 	bit_mask = (bit_mask << 8) | data[2];
 	bit_mask = (bit_mask << 8) | data[3];
 
+	/* robustness in case input_mapping hook does not get called */
+	if (!pm->input_ep82)
+		return 0;
+
 	/* break keys */
 	for (bit_index = 0; bit_index < 24; bit_index++) {
 		if (!((0x01 << bit_index) & bit_mask)) {
-- 
2.52.0.457.g6b5491de43-goog


^ permalink raw reply related

* [PATCH] HID: magicmouse: Do not crash on missing msc->input
From: Günther Noack @ 2026-01-09 10:57 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Günther Noack, stable, linux-input, linux-kernel

Fake USB devices can send their own report descriptors for which the
input_mapping() hook does not get called.  In this case, msc->input stays NULL,
leading to a crash at a later time.

Detect this condition in the input_configured() hook and reject the device.

This is not supposed to happen with actual magic mouse devices, but can be
provoked by imposing as a magic mouse USB device.

Cc: stable@vger.kernel.org
Signed-off-by: Günther Noack <gnoack@google.com>
---
 drivers/hid/hid-magicmouse.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/drivers/hid/hid-magicmouse.c b/drivers/hid/hid-magicmouse.c
index 6e7c189f4d1d..b8932f02b6ee 100644
--- a/drivers/hid/hid-magicmouse.c
+++ b/drivers/hid/hid-magicmouse.c
@@ -726,6 +726,11 @@ static int magicmouse_input_configured(struct hid_device *hdev,
 	struct magicmouse_sc *msc = hid_get_drvdata(hdev);
 	int ret;
 
+	if (!msc->input) {
+		hid_err(hdev, "magicmouse setup input failed (no input)");
+		return -EINVAL;
+	}
+
 	ret = magicmouse_setup_input(msc->input, hdev);
 	if (ret) {
 		hid_err(hdev, "magicmouse setup input failed (%d)\n", ret);
-- 
2.52.0.457.g6b5491de43-goog


^ permalink raw reply related

* Re: [PATCH v2 3/6] dt-bindings: input: google,goldfish-events-keypad: Convert to DT schema
From: Krzysztof Kozlowski @ 2026-01-09  8:23 UTC (permalink / raw)
  To: Kuan-Wei Chiu
  Cc: airlied, simona, maarten.lankhorst, mripard, tzimmermann, robh,
	krzk+dt, conor+dt, dmitry.torokhov, sre, gregkh, jirislaby,
	lgirdwood, broonie, jserv, eleanor15x, dri-devel, devicetree,
	linux-kernel, linux-input, linux-pm, linux-serial, linux-sound
In-Reply-To: <20260108080836.3777829-4-visitorckw@gmail.com>

On Thu, Jan 08, 2026 at 08:08:33AM +0000, Kuan-Wei Chiu wrote:
> +
> +examples:
> +  - |
> +    keypad@9040000 {
> +        compatible = "google,goldfish-events-keypad";
> +        reg = <0x9040000 0x1000>;
> +        interrupts = <0x5>;

Same comment as before. It applies everywhere, btw.

> +    };
> -- 
> 2.52.0.457.g6b5491de43-goog
> 

^ permalink raw reply

* Re: [PATCH v2 1/6] dt-bindings: serial: google,goldfish-tty: Convert to DT schema
From: Krzysztof Kozlowski @ 2026-01-09  8:20 UTC (permalink / raw)
  To: Kuan-Wei Chiu
  Cc: airlied, simona, maarten.lankhorst, mripard, tzimmermann, robh,
	krzk+dt, conor+dt, dmitry.torokhov, sre, gregkh, jirislaby,
	lgirdwood, broonie, jserv, eleanor15x, dri-devel, devicetree,
	linux-kernel, linux-input, linux-pm, linux-serial, linux-sound
In-Reply-To: <20260108080836.3777829-2-visitorckw@gmail.com>

On Thu, Jan 08, 2026 at 08:08:31AM +0000, Kuan-Wei Chiu wrote:
> Convert the Google Goldfish TTY binding to DT schema format.
> Move the file to the serial directory to match the subsystem.
> Update the example node name to 'serial' to comply with generic node
> naming standards.
> 
> Signed-off-by: Kuan-Wei Chiu <visitorckw@gmail.com>
> ---
> Changes in v2:
> - Add reference to serial.yaml schema.
> - Change additionalProperties to unevaluatedProperties: false.

Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>

Best regards,
Krzysztof


^ permalink raw reply

* [PATCH] HID: mcp2221: free I2C bus after failed transactions
From: Sven Zühlsdorf @ 2026-01-09  7:00 UTC (permalink / raw)
  To: Rishi Gupta, Jiri Kosina, Benjamin Tissoires, linux-i2c,
	linux-input, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 2360 bytes --]

The conversion of unnecessary speed reconfigurations to actual error cleanup
has missed the timeout case in mcp_i2c_smbus_read. While we give up on
waiting for the requested data, the chip is still executing the transaction.
Attempts to initiate further transactions will be rejected with
"I2C engine busy" (translated to -EAGAIN by the driver). This can be
reproduced by attempting a read from a nonexistent device, as e.g. i2cdetect
does by default for the 0x30-0x3f and 0x50-0x5f address ranges.
Explicitly canceling the current transaction fixes this.

In addition, when initiating an I2C transaction and the chip accepts the
command, i.e. doesn't return "I2C engine busy"/-EAGAIN, we still need to
explicitly free the bus, if we detect an error condition in the HID
report's I2C engine state.

Fixes: 02a46753601a ("HID: mcp2221: Don't set bus speed on every transfer")
Signed-off-by: Sven Zühlsdorf <sven.zuehlsdorf@vigem.de>
---
  drivers/hid/hid-mcp2221.c | 19 ++++++++++++++++---
  1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/drivers/hid/hid-mcp2221.c b/drivers/hid/hid-mcp2221.c
index 33603b019f975..50237b31c781c 100644
--- a/drivers/hid/hid-mcp2221.c
+++ b/drivers/hid/hid-mcp2221.c
@@ -274,8 +274,11 @@ static int mcp_i2c_write(struct mcp2221 *mcp,
  		memcpy(&mcp->txbuf[4], &msg->buf[idx], len);

  		ret = mcp_send_data_req_status(mcp, mcp->txbuf, len + 4);
-		if (ret)
+		if (ret) {
+			if (ret != -EAGAIN)
+				mcp_cancel_last_cmd(mcp);
  			return ret;
+		}

  		usleep_range(980, 1000);

@@ -332,8 +335,11 @@ static int mcp_i2c_smbus_read(struct mcp2221 *mcp,
  	}

  	ret = mcp_send_data_req_status(mcp, mcp->txbuf, 4);
-	if (ret)
+	if (ret) {
+		if (ret != -EAGAIN)
+			mcp_cancel_last_cmd(mcp);
  		return ret;
+	}

  	mcp->rxbuf_idx = 0;

@@ -353,6 +359,10 @@ static int mcp_i2c_smbus_read(struct mcp2221 *mcp,
  				usleep_range(90, 100);
  				retries++;
  			} else {
+				/* Too many retries.
+				 * Give up and free the bus.
+				 */
+				mcp_cancel_last_cmd(mcp);
  				return ret;
  			}
  		} else {
@@ -454,8 +464,11 @@ static int mcp_smbus_write(struct mcp2221 *mcp, u16 addr,
  	}

  	ret = mcp_send_data_req_status(mcp, mcp->txbuf, data_len);
-	if (ret)
+	if (ret) {
+		if (ret != -EAGAIN)
+			mcp_cancel_last_cmd(mcp);
  		return ret;
+	}

  	if (last_status) {
  		usleep_range(980, 1000);
-- 
2.52.0

^ permalink raw reply related

* [dtor-input:next] BUILD SUCCESS 71ed55143d9dba39b564d63d89411b07ef294c58
From: kernel test robot @ 2026-01-09  6:11 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input

tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git next
branch HEAD: 71ed55143d9dba39b564d63d89411b07ef294c58  Input: twl4030 - fix warnings without CONFIG_OF

elapsed time: 1452m

configs tested: 221
configs skipped: 5

The following configs have been built successfully.
More configs may be tested in the coming days.

tested configs:
alpha                             allnoconfig    gcc-15.1.0
alpha                            allyesconfig    gcc-15.2.0
alpha                               defconfig    gcc-15.1.0
arc                              allmodconfig    clang-16
arc                               allnoconfig    gcc-15.1.0
arc                              allyesconfig    clang-22
arc                          axs103_defconfig    gcc-15.1.0
arc                                 defconfig    gcc-15.1.0
arc                   randconfig-001-20260109    gcc-13.4.0
arc                   randconfig-002-20260109    gcc-13.4.0
arc                   randconfig-002-20260109    gcc-9.5.0
arm                               allnoconfig    gcc-15.1.0
arm                              allyesconfig    clang-16
arm                         at91_dt_defconfig    clang-22
arm                         axm55xx_defconfig    clang-22
arm                                 defconfig    clang-22
arm                          pxa3xx_defconfig    clang-22
arm                   randconfig-001-20260109    gcc-10.5.0
arm                   randconfig-001-20260109    gcc-13.4.0
arm                   randconfig-002-20260109    gcc-13.4.0
arm                   randconfig-002-20260109    gcc-15.1.0
arm                   randconfig-003-20260109    gcc-13.4.0
arm                   randconfig-003-20260109    gcc-8.5.0
arm                   randconfig-004-20260109    gcc-11.5.0
arm                   randconfig-004-20260109    gcc-13.4.0
arm64                            allmodconfig    clang-22
arm64                             allnoconfig    gcc-15.1.0
arm64                               defconfig    gcc-15.1.0
arm64                 randconfig-001-20260109    clang-22
arm64                 randconfig-001-20260109    gcc-8.5.0
arm64                 randconfig-002-20260109    clang-22
arm64                 randconfig-002-20260109    gcc-8.5.0
arm64                 randconfig-003-20260109    gcc-11.5.0
arm64                 randconfig-003-20260109    gcc-8.5.0
arm64                 randconfig-004-20260109    gcc-8.5.0
csky                             allmodconfig    gcc-15.2.0
csky                              allnoconfig    gcc-15.1.0
csky                                defconfig    gcc-15.1.0
csky                  randconfig-001-20260109    gcc-8.5.0
csky                  randconfig-001-20260109    gcc-9.5.0
csky                  randconfig-002-20260109    gcc-15.1.0
csky                  randconfig-002-20260109    gcc-8.5.0
hexagon                          alldefconfig    clang-22
hexagon                          allmodconfig    gcc-15.2.0
hexagon                           allnoconfig    gcc-15.1.0
hexagon                             defconfig    clang-22
hexagon               randconfig-001-20260109    clang-22
hexagon               randconfig-001-20260109    gcc-8.5.0
hexagon               randconfig-002-20260109    clang-17
hexagon               randconfig-002-20260109    gcc-8.5.0
i386                             allmodconfig    clang-20
i386                              allnoconfig    gcc-15.1.0
i386                             allyesconfig    clang-20
i386        buildonly-randconfig-001-20260109    clang-20
i386        buildonly-randconfig-002-20260109    clang-20
i386        buildonly-randconfig-002-20260109    gcc-14
i386        buildonly-randconfig-003-20260109    clang-20
i386        buildonly-randconfig-003-20260109    gcc-14
i386        buildonly-randconfig-004-20260109    clang-20
i386        buildonly-randconfig-005-20260109    clang-20
i386        buildonly-randconfig-005-20260109    gcc-12
i386        buildonly-randconfig-006-20260109    clang-20
i386                                defconfig    clang-20
i386                  randconfig-001-20260109    gcc-14
i386                  randconfig-002-20260109    gcc-14
i386                  randconfig-003-20260109    gcc-12
i386                  randconfig-003-20260109    gcc-14
i386                  randconfig-004-20260109    gcc-14
i386                  randconfig-005-20260109    gcc-14
i386                  randconfig-006-20260109    clang-20
i386                  randconfig-006-20260109    gcc-14
i386                  randconfig-007-20260109    gcc-14
i386                  randconfig-011-20260109    clang-20
i386                  randconfig-012-20260109    gcc-14
i386                  randconfig-013-20260109    clang-20
i386                  randconfig-014-20260109    gcc-14
i386                  randconfig-015-20260109    clang-20
i386                  randconfig-016-20260109    clang-20
i386                  randconfig-017-20260109    clang-20
loongarch                        allmodconfig    clang-22
loongarch                         allnoconfig    gcc-15.1.0
loongarch                           defconfig    clang-19
loongarch             randconfig-001-20260109    clang-22
loongarch             randconfig-001-20260109    gcc-8.5.0
loongarch             randconfig-002-20260109    gcc-15.1.0
loongarch             randconfig-002-20260109    gcc-8.5.0
m68k                             allmodconfig    gcc-15.2.0
m68k                              allnoconfig    gcc-15.1.0
m68k                             allyesconfig    clang-16
m68k                                defconfig    clang-19
m68k                                defconfig    gcc-15.1.0
microblaze                        allnoconfig    gcc-15.1.0
microblaze                       allyesconfig    gcc-15.2.0
microblaze                          defconfig    clang-19
microblaze                          defconfig    gcc-15.1.0
mips                             allmodconfig    gcc-15.2.0
mips                              allnoconfig    gcc-15.1.0
mips                             allyesconfig    gcc-15.2.0
mips                        vocore2_defconfig    clang-22
nios2                            allmodconfig    clang-22
nios2                             allnoconfig    clang-22
nios2                               defconfig    clang-19
nios2                               defconfig    gcc-11.5.0
nios2                 randconfig-001-20260109    gcc-8.5.0
nios2                 randconfig-002-20260109    gcc-8.5.0
openrisc                         allmodconfig    clang-22
openrisc                          allnoconfig    clang-22
openrisc                            defconfig    gcc-15.1.0
openrisc                            defconfig    gcc-15.2.0
parisc                           allmodconfig    gcc-15.2.0
parisc                            allnoconfig    clang-22
parisc                              defconfig    gcc-15.1.0
parisc                              defconfig    gcc-15.2.0
parisc                randconfig-001-20260109    gcc-12.5.0
parisc                randconfig-002-20260109    gcc-15.1.0
parisc64                            defconfig    clang-19
parisc64                            defconfig    gcc-15.1.0
powerpc                          allmodconfig    gcc-15.2.0
powerpc                           allnoconfig    clang-22
powerpc                  mpc866_ads_defconfig    clang-22
powerpc               randconfig-001-20260109    clang-22
powerpc               randconfig-002-20260109    clang-22
powerpc64             randconfig-001-20260109    gcc-15.1.0
powerpc64             randconfig-002-20260109    gcc-8.5.0
riscv                            allmodconfig    clang-22
riscv                             allnoconfig    clang-22
riscv                            allyesconfig    clang-16
riscv                               defconfig    clang-22
riscv                               defconfig    gcc-15.2.0
riscv                 randconfig-001-20260109    clang-22
riscv                 randconfig-001-20260109    gcc-14.3.0
riscv                 randconfig-002-20260109    clang-22
riscv                 randconfig-002-20260109    gcc-8.5.0
s390                              allnoconfig    clang-22
s390                             allyesconfig    gcc-15.2.0
s390                                defconfig    clang-22
s390                                defconfig    gcc-15.2.0
s390                  randconfig-001-20260109    clang-22
s390                  randconfig-002-20260109    clang-22
sh                               allmodconfig    gcc-15.2.0
sh                                allnoconfig    clang-22
sh                                  defconfig    gcc-15.1.0
sh                    randconfig-001-20260109    clang-22
sh                    randconfig-001-20260109    gcc-14.3.0
sh                    randconfig-002-20260109    clang-22
sh                    randconfig-002-20260109    gcc-13.4.0
sparc                             allnoconfig    clang-22
sparc                               defconfig    gcc-15.1.0
sparc                               defconfig    gcc-15.2.0
sparc                 randconfig-001-20260109    gcc-14.3.0
sparc                 randconfig-001-20260109    gcc-8.5.0
sparc                 randconfig-002-20260109    gcc-13.4.0
sparc                 randconfig-002-20260109    gcc-14.3.0
sparc64                          allmodconfig    clang-22
sparc64                             defconfig    clang-20
sparc64               randconfig-001-20260109    gcc-14.3.0
sparc64               randconfig-001-20260109    gcc-8.5.0
sparc64               randconfig-002-20260109    clang-20
sparc64               randconfig-002-20260109    gcc-14.3.0
um                                allnoconfig    clang-22
um                               allyesconfig    gcc-15.2.0
um                                  defconfig    clang-22
um                             i386_defconfig    gcc-14
um                    randconfig-001-20260109    gcc-14
um                    randconfig-001-20260109    gcc-14.3.0
um                    randconfig-002-20260109    gcc-14
um                    randconfig-002-20260109    gcc-14.3.0
um                           x86_64_defconfig    clang-22
x86_64                           allmodconfig    clang-20
x86_64                            allnoconfig    clang-22
x86_64                           allyesconfig    clang-20
x86_64      buildonly-randconfig-001-20260109    clang-20
x86_64      buildonly-randconfig-001-20260109    gcc-14
x86_64      buildonly-randconfig-002-20260109    gcc-14
x86_64      buildonly-randconfig-003-20260109    gcc-14
x86_64      buildonly-randconfig-004-20260109    gcc-14
x86_64      buildonly-randconfig-005-20260109    gcc-14
x86_64      buildonly-randconfig-006-20260109    gcc-14
x86_64                              defconfig    gcc-14
x86_64                randconfig-001-20260109    clang-20
x86_64                randconfig-001-20260109    gcc-14
x86_64                randconfig-002-20260109    clang-20
x86_64                randconfig-002-20260109    gcc-14
x86_64                randconfig-003-20260109    gcc-14
x86_64                randconfig-004-20260109    clang-20
x86_64                randconfig-004-20260109    gcc-14
x86_64                randconfig-005-20260109    clang-20
x86_64                randconfig-005-20260109    gcc-14
x86_64                randconfig-006-20260109    gcc-14
x86_64                randconfig-011-20260109    gcc-12
x86_64                randconfig-011-20260109    gcc-14
x86_64                randconfig-012-20260109    clang-20
x86_64                randconfig-012-20260109    gcc-14
x86_64                randconfig-013-20260109    gcc-14
x86_64                randconfig-014-20260109    gcc-14
x86_64                randconfig-015-20260109    gcc-12
x86_64                randconfig-015-20260109    gcc-14
x86_64                randconfig-016-20260109    gcc-14
x86_64                randconfig-071-20260109    clang-20
x86_64                randconfig-071-20260109    gcc-14
x86_64                randconfig-072-20260109    clang-20
x86_64                randconfig-073-20260109    clang-20
x86_64                randconfig-073-20260109    gcc-14
x86_64                randconfig-074-20260109    clang-20
x86_64                randconfig-074-20260109    gcc-14
x86_64                randconfig-075-20260109    clang-20
x86_64                randconfig-075-20260109    gcc-14
x86_64                randconfig-076-20260109    clang-20
x86_64                randconfig-076-20260109    gcc-12
x86_64                               rhel-9.4    gcc-14
x86_64                           rhel-9.4-bpf    gcc-14
x86_64                          rhel-9.4-func    gcc-14
x86_64                    rhel-9.4-kselftests    gcc-14
x86_64                         rhel-9.4-kunit    gcc-14
x86_64                           rhel-9.4-ltp    gcc-14
x86_64                          rhel-9.4-rust    clang-20
xtensa                            allnoconfig    clang-22
xtensa                           allyesconfig    clang-22
xtensa                randconfig-001-20260109    gcc-12.5.0
xtensa                randconfig-001-20260109    gcc-14.3.0
xtensa                randconfig-002-20260109    gcc-14.3.0

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* [PATCH 2/2] Input: edt-ft5x06 - add support for polling mode
From: Marek Vasut @ 2026-01-09  3:51 UTC (permalink / raw)
  To: linux-input
  Cc: Marek Vasut, Conor Dooley, Dmitry Torokhov, Jens Reidel,
	Joel Selvaraj, Krzysztof Kozlowski, Rob Herring, Wolfram Sang,
	devicetree, kernel, linux-kernel
In-Reply-To: <20260109035149.1341931-1-marex@nabladev.com>

There are designs incorporating EDT ETM touch controller that do not
connect interrupt pin, for example Raspberry Pi. To support such systems
use polling mode for the input device when I2C client does not have
interrupt assigned to it.

Signed-off-by: Marek Vasut <marex@nabladev.com>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: Dmitry Torokhov <dmitry.torokhov@gmail.com>
Cc: Jens Reidel <adrian@mainlining.org>
Cc: Joel Selvaraj <foss@joelselvaraj.com>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Rob Herring <robh@kernel.org>
Cc: Wolfram Sang <wsa+renesas@sang-engineering.com>
Cc: devicetree@vger.kernel.org
Cc: kernel@dh-electronics.com
Cc: linux-input@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
---
 drivers/input/touchscreen/edt-ft5x06.c | 74 ++++++++++++++++++--------
 1 file changed, 53 insertions(+), 21 deletions(-)

diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
index 9a0add3d39159..a16bf93243976 100644
--- a/drivers/input/touchscreen/edt-ft5x06.c
+++ b/drivers/input/touchscreen/edt-ft5x06.c
@@ -69,6 +69,7 @@
 #define TOUCH_EVENT_RESERVED		0x03
 
 #define EDT_NAME_LEN			23
+#define EDT_POLL_INTERVAL_MS		17 /* msec */
 #define EDT_SWITCH_MODE_RETRIES		10
 #define EDT_SWITCH_MODE_DELAY		5 /* msec */
 #define EDT_RAW_DATA_RETRIES		100
@@ -295,9 +296,9 @@ static const struct regmap_config edt_M06_i2c_regmap_config = {
 	.write = edt_M06_i2c_write,
 };
 
-static irqreturn_t edt_ft5x06_ts_isr(int irq, void *dev_id)
+static void edt_ft5x06_ts_process_events(struct edt_ft5x06_ts_data *tsdata,
+					 bool poll)
 {
-	struct edt_ft5x06_ts_data *tsdata = dev_id;
 	struct device *dev = &tsdata->client->dev;
 	u8 rdbuf[63];
 	int i, type, x, y, id;
@@ -307,9 +308,13 @@ static irqreturn_t edt_ft5x06_ts_isr(int irq, void *dev_id)
 	error = regmap_bulk_read(tsdata->regmap, tsdata->tdata_cmd, rdbuf,
 				 tsdata->tdata_len);
 	if (error) {
-		dev_err_ratelimited(dev, "Unable to fetch data, error: %d\n",
-				    error);
-		goto out;
+		if (!poll) {
+			/* Polling may result in no data. */
+			dev_err_ratelimited(dev,
+					    "Unable to fetch data, error: %d\n",
+					    error);
+		}
+		return;
 	}
 
 	for (i = 0; i < tsdata->max_support_points; i++) {
@@ -341,11 +346,24 @@ static irqreturn_t edt_ft5x06_ts_isr(int irq, void *dev_id)
 
 	input_mt_report_pointer_emulation(tsdata->input, true);
 	input_sync(tsdata->input);
+}
+
+static irqreturn_t edt_ft5x06_ts_isr(int irq, void *dev_id)
+{
+	struct edt_ft5x06_ts_data *tsdata = dev_id;
+
+	edt_ft5x06_ts_process_events(tsdata, false);
 
-out:
 	return IRQ_HANDLED;
 }
 
+static void edt_ft5x06_work_i2c_poll(struct input_dev *input)
+{
+	struct edt_ft5x06_ts_data *tsdata = input_get_drvdata(input);
+
+	edt_ft5x06_ts_process_events(tsdata, true);
+}
+
 struct edt_ft5x06_attribute {
 	struct device_attribute dattr;
 	size_t field_offset;
@@ -613,7 +631,8 @@ static int edt_ft5x06_factory_mode(struct edt_ft5x06_ts_data *tsdata)
 		return -EINVAL;
 	}
 
-	disable_irq(client->irq);
+	if (client->irq)
+		disable_irq(client->irq);
 
 	if (!tsdata->raw_buffer) {
 		tsdata->raw_bufsize = tsdata->num_x * tsdata->num_y *
@@ -656,7 +675,8 @@ static int edt_ft5x06_factory_mode(struct edt_ft5x06_ts_data *tsdata)
 	kfree(tsdata->raw_buffer);
 	tsdata->raw_buffer = NULL;
 	tsdata->factory_mode = false;
-	enable_irq(client->irq);
+	if (client->irq)
+		enable_irq(client->irq);
 
 	return error;
 }
@@ -697,7 +717,8 @@ static int edt_ft5x06_work_mode(struct edt_ft5x06_ts_data *tsdata)
 	tsdata->raw_buffer = NULL;
 
 	edt_ft5x06_restore_reg_parameters(tsdata);
-	enable_irq(client->irq);
+	if (client->irq)
+		enable_irq(client->irq);
 
 	return 0;
 }
@@ -1331,17 +1352,26 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
 		return error;
 	}
 
-	irq_flags = irq_get_trigger_type(client->irq);
-	if (irq_flags == IRQF_TRIGGER_NONE)
-		irq_flags = IRQF_TRIGGER_FALLING;
-	irq_flags |= IRQF_ONESHOT;
+	input_set_drvdata(input, tsdata);
 
-	error = devm_request_threaded_irq(&client->dev, client->irq,
-					  NULL, edt_ft5x06_ts_isr, irq_flags,
-					  client->name, tsdata);
-	if (error) {
-		dev_err(&client->dev, "Unable to request touchscreen IRQ.\n");
-		return error;
+	if (client->irq) {
+		irq_flags = irq_get_trigger_type(client->irq);
+		if (irq_flags == IRQF_TRIGGER_NONE)
+			irq_flags = IRQF_TRIGGER_FALLING;
+		irq_flags |= IRQF_ONESHOT;
+
+		error = devm_request_threaded_irq(&client->dev, client->irq,
+						  NULL, edt_ft5x06_ts_isr, irq_flags,
+						  client->name, tsdata);
+		if (error) {
+			dev_err(&client->dev, "Unable to request touchscreen IRQ.\n");
+			return error;
+		}
+	} else {
+		error = input_setup_polling(input, edt_ft5x06_work_i2c_poll);
+		if (error)
+			return dev_err_probe(&client->dev, error, "Unable to set up polling.\n");
+		input_set_poll_interval(input, EDT_POLL_INTERVAL_MS);
 	}
 
 	error = input_register_device(input);
@@ -1394,7 +1424,8 @@ static int edt_ft5x06_ts_suspend(struct device *dev)
 	 * settings. Disable the irq to avoid adjusting each host till the
 	 * device is back in a full functional state.
 	 */
-	disable_irq(tsdata->client->irq);
+	if (tsdata->client->irq)
+		disable_irq(tsdata->client->irq);
 
 	gpiod_set_value_cansleep(reset_gpio, 1);
 	usleep_range(1000, 2000);
@@ -1456,7 +1487,8 @@ static int edt_ft5x06_ts_resume(struct device *dev)
 		msleep(300);
 
 		edt_ft5x06_restore_reg_parameters(tsdata);
-		enable_irq(tsdata->client->irq);
+		if (tsdata->client->irq)
+			enable_irq(tsdata->client->irq);
 
 		if (tsdata->factory_mode)
 			ret = edt_ft5x06_factory_mode(tsdata);
-- 
2.51.0


^ permalink raw reply related

* [PATCH 1/2] dt-bindings: input: touchscreen: edt-ft5x06: Drop 'interrupts' requirement
From: Marek Vasut @ 2026-01-09  3:51 UTC (permalink / raw)
  To: linux-input
  Cc: Marek Vasut, Conor Dooley, Dmitry Torokhov, Jens Reidel,
	Joel Selvaraj, Krzysztof Kozlowski, Rob Herring, Wolfram Sang,
	devicetree, kernel, linux-kernel

The interrupts property is optional, since at least the Linux kernel driver
supports also polling mode. Drop the interrupts property requirement.

Signed-off-by: Marek Vasut <marex@nabladev.com>
---
Cc: Conor Dooley <conor+dt@kernel.org>
Cc: Dmitry Torokhov <dmitry.torokhov@gmail.com>
Cc: Jens Reidel <adrian@mainlining.org>
Cc: Joel Selvaraj <foss@joelselvaraj.com>
Cc: Krzysztof Kozlowski <krzk+dt@kernel.org>
Cc: Rob Herring <robh@kernel.org>
Cc: Wolfram Sang <wsa+renesas@sang-engineering.com>
Cc: devicetree@vger.kernel.org
Cc: kernel@dh-electronics.com
Cc: linux-input@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
---
 .../devicetree/bindings/input/touchscreen/edt-ft5x06.yaml        | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Documentation/devicetree/bindings/input/touchscreen/edt-ft5x06.yaml b/Documentation/devicetree/bindings/input/touchscreen/edt-ft5x06.yaml
index 7d3edb58f72d8..1c6d3c674841e 100644
--- a/Documentation/devicetree/bindings/input/touchscreen/edt-ft5x06.yaml
+++ b/Documentation/devicetree/bindings/input/touchscreen/edt-ft5x06.yaml
@@ -111,7 +111,6 @@ unevaluatedProperties: false
 required:
   - compatible
   - reg
-  - interrupts
 
 examples:
   - |
-- 
2.51.0


^ permalink raw reply related

* [PATCH v2 1/1] HID: switch2: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-01-09  3:40 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Luiz Augusto von Dentz, Bastien Nocera
In-Reply-To: <20260109034034.565630-1-vi@endrift.com>

This adds a new driver for the Switch 2 controllers. The Switch 2 uses an
unusual split-interface design such that input and rumble occur on the main
HID interface, but all other communication occurs over a "configuration"
interface. This is the case on both USB and Bluetooth, so this new driver
uses a split-driver design with the HID interface being the "main" driver
and the configuration interface is a secondary driver that looks up to the
HID interface, sharing resources on a common struct.

Due to using a non-standard pairing interface as well as Bluetooth
communications being extremely limited in the kernel, a custom interface
between userspace and the kernel will need to be design, along with bringup
in BlueZ. That is beyond the scope of this initial patch, which only
contains the generic HID and USB configuration interface drivers.

This initial work supports general input for the Joy-Con 2, Pro Controller
2, and GameCube NSO controllers, as well as basic rumble support. IMU
support is not yet present.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 MAINTAINERS                          |   7 +
 drivers/hid/Kconfig                  |  18 +
 drivers/hid/Makefile                 |   1 +
 drivers/hid/hid-ids.h                |   4 +
 drivers/hid/hid-switch2.c            | 975 +++++++++++++++++++++++++++
 drivers/hid/hid-switch2.h            | 242 +++++++
 drivers/input/joystick/Kconfig       |   8 +
 drivers/input/joystick/Makefile      |   1 +
 drivers/input/joystick/switch2-usb.c | 353 ++++++++++
 9 files changed, 1609 insertions(+)
 create mode 100644 drivers/hid/hid-switch2.c
 create mode 100644 drivers/hid/hid-switch2.h
 create mode 100644 drivers/input/joystick/switch2-usb.c

diff --git a/MAINTAINERS b/MAINTAINERS
index a0dd762f5648..932e73d80bbb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18541,6 +18541,13 @@ L:	linux-input@vger.kernel.org
 S:	Maintained
 F:	drivers/hid/hid-nintendo*
 
+NINTENDO SWITCH 2 CONTROLLER DRIVER
+M:	Vicki Pfau <vi@endrift.com>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	drivers/hid/hid-switch2*
+F:	drivers/input/joystick/switch2-usb.c
+
 NIOS2 ARCHITECTURE
 M:	Dinh Nguyen <dinguyen@kernel.org>
 S:	Maintained
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 920a64b66b25..32190cf830c4 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1142,6 +1142,24 @@ config HID_SUNPLUS
 	help
 	Support for Sunplus wireless desktop.
 
+config HID_SWITCH2
+	tristate "Nintendo Switch 2 controller support"
+	depends on HID
+	select USB_HID
+	help
+	Adds support for the Nintendo Switch 2 Controllers.
+
+	To compile this driver as a module, choose M here: the
+	module will be called hid-switch2.
+
+config SWITCH2_FF
+	bool "Nintendo Switch 2 force feedback support"
+	depends on HID_SWITCH2
+	select INPUT_FF_MEMLESS
+	help
+	Say Y here if you want to enable force feedback support for Nintendo
+	Switch 2 game controllers.
+
 config HID_RMI
 	tristate "Synaptics RMI4 device support"
 	select RMI4_CORE
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 361a7daedeb8..74a3f82ed82f 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -131,6 +131,7 @@ obj-$(CONFIG_HID_SPEEDLINK)	+= hid-speedlink.o
 obj-$(CONFIG_HID_STEAM)		+= hid-steam.o
 obj-$(CONFIG_HID_STEELSERIES)	+= hid-steelseries.o
 obj-$(CONFIG_HID_SUNPLUS)	+= hid-sunplus.o
+obj-$(CONFIG_HID_SWITCH2)	+= hid-switch2.o
 obj-$(CONFIG_HID_GREENASIA)	+= hid-gaff.o
 obj-$(CONFIG_HID_THRUSTMASTER)	+= hid-tmff.o hid-thrustmaster.o
 obj-$(CONFIG_HID_TIVO)		+= hid-tivo.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 9c2bf584d9f6..595f94543d85 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1061,6 +1061,10 @@
 #define USB_DEVICE_ID_NINTENDO_SNESCON	0x2017
 #define USB_DEVICE_ID_NINTENDO_GENCON	0x201e
 #define USB_DEVICE_ID_NINTENDO_N64CON	0x2019
+#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONR	0x2066
+#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONL	0x2067
+#define USB_DEVICE_ID_NINTENDO_NS2_PROCON	0x2069
+#define USB_DEVICE_ID_NINTENDO_NS2_GCCON	0x2073
 
 #define USB_VENDOR_ID_NOVATEK		0x0603
 #define USB_DEVICE_ID_NOVATEK_PCT	0x0600
diff --git a/drivers/hid/hid-switch2.c b/drivers/hid/hid-switch2.c
new file mode 100644
index 000000000000..aa1b46501a1a
--- /dev/null
+++ b/drivers/hid/hid-switch2.c
@@ -0,0 +1,975 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * HID driver for Nintendo Switch controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the following work:
+ *   https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ *   https://github.com/ndeadly/switch2_controller_research
+ *   hid-nintendo driver
+ */
+
+#include "hid-switch2.h"
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/idr.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/usb.h>
+#include "usbhid/usbhid.h"
+
+#define NS2_BTNR_B	BIT(0)
+#define NS2_BTNR_A	BIT(1)
+#define NS2_BTNR_Y	BIT(2)
+#define NS2_BTNR_X	BIT(3)
+#define NS2_BTNR_R	BIT(4)
+#define NS2_BTNR_ZR	BIT(5)
+#define NS2_BTNR_PLUS	BIT(6)
+#define NS2_BTNR_RS	BIT(7)
+
+#define NS2_BTNL_DOWN	BIT(0)
+#define NS2_BTNL_RIGHT	BIT(1)
+#define NS2_BTNL_LEFT	BIT(2)
+#define NS2_BTNL_UP	BIT(3)
+#define NS2_BTNL_L	BIT(4)
+#define NS2_BTNL_ZL	BIT(5)
+#define NS2_BTNL_MINUS	BIT(6)
+#define NS2_BTNL_LS	BIT(7)
+
+#define NS2_BTN3_C	BIT(4)
+#define NS2_BTN3_SR	BIT(6)
+#define NS2_BTN3_SL	BIT(7)
+
+#define NS2_BTN_JCR_HOME	BIT(0)
+#define NS2_BTN_JCR_GR		BIT(2)
+#define NS2_BTN_JCR_C		NS2_BTN3_C
+#define NS2_BTN_JCR_SR		NS2_BTN3_SR
+#define NS2_BTN_JCR_SL		NS2_BTN3_SL
+
+#define NS2_BTN_JCL_CAPTURE	BIT(0)
+#define NS2_BTN_JCL_GL		BIT(2)
+#define NS2_BTN_JCL_SR		NS2_BTN3_SR
+#define NS2_BTN_JCL_SL		NS2_BTN3_SL
+
+#define NS2_BTN_PRO_HOME	BIT(0)
+#define NS2_BTN_PRO_CAPTURE	BIT(1)
+#define NS2_BTN_PRO_GR		BIT(2)
+#define NS2_BTN_PRO_GL		BIT(3)
+#define NS2_BTN_PRO_C		NS2_BTN3_C
+
+#define NS2_BTN_GC_HOME		BIT(0)
+#define NS2_BTN_GC_CAPTURE	BIT(1)
+#define NS2_BTN_GC_C		NS2_BTN3_C
+
+#define NS2_TRIGGER_RANGE	4095
+#define NS2_AXIS_MIN		-32768
+#define NS2_AXIS_MAX		32767
+
+#define NS2_MAX_PLAYER_ID	8
+
+enum gc_rumble {
+	GC_RUMBLE_OFF = 0,
+	GC_RUMBLE_ON = 1,
+	GC_RUMBLE_STOP = 2,
+};
+
+/*
+ * The highest rumble level for "HD Rumble" is strong enough to potentially damage the controller,
+ * and also leaves your hands feeling like melted jelly, so we set a semi-abitrary scaling factor
+ * to artificially limit the maximum for safety and comfort. It is currently unknown if the Switch
+ * 2 itself does something similar, but it's quite likely.
+ *
+ * This value must be between 0 and 1024, otherwise the math below will overflow.
+ */
+#define RUMBLE_MAX 450u
+
+/*
+ * Semi-arbitrary values used to simulate the "rumble" sensation of an eccentric rotating
+ * mass type haptic motor on the Switch 2 controllers' linear resonant actuator type haptics.
+ *
+ * The units used are unknown, but the values must be between 0 and 1023.
+ */
+#define RUMBLE_HI_FREQ 0x187
+#define RUMBLE_LO_FREQ 0x112
+
+static DEFINE_MUTEX(switch2_controllers_lock);
+static LIST_HEAD(switch2_controllers);
+
+struct switch2_ctlr_button_mapping {
+	uint32_t code;
+	int byte;
+	uint32_t bit;
+};
+
+static const struct switch2_ctlr_button_mapping left_joycon_button_mappings[] = {
+	{ BTN_TL,	0, NS2_BTNL_L,		},
+	{ BTN_TL2,	0, NS2_BTNL_ZL,		},
+	{ BTN_SELECT,	0, NS2_BTNL_MINUS,	},
+	{ BTN_THUMBL,	0, NS2_BTNL_LS,		},
+	{ BTN_GRIPL,	1, NS2_BTN_JCL_GL,	},
+	{ KEY_RECORD,	1, NS2_BTN_JCL_CAPTURE,	},
+	{ /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping right_joycon_button_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
+	{ BTN_EAST,	0, NS2_BTNR_B,		},
+	{ BTN_NORTH,	0, NS2_BTNR_X,		},
+	{ BTN_WEST,	0, NS2_BTNR_Y,		},
+	{ BTN_TR,	0, NS2_BTNR_R,		},
+	{ BTN_TR2,	0, NS2_BTNR_ZR,		},
+	{ BTN_START,	0, NS2_BTNR_PLUS,	},
+	{ BTN_THUMBR,	0, NS2_BTNR_RS,		},
+	{ BTN_C,	1, NS2_BTN_JCR_C,	},
+	{ BTN_GRIPR,	1, NS2_BTN_JCR_GR,	},
+	{ BTN_MODE,	1, NS2_BTN_JCR_HOME,	},
+	{ /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping procon_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
+	{ BTN_EAST,	0, NS2_BTNR_B,		},
+	{ BTN_NORTH,	0, NS2_BTNR_X,		},
+	{ BTN_WEST,	0, NS2_BTNR_Y,		},
+	{ BTN_TL,	1, NS2_BTNL_L,		},
+	{ BTN_TR,	0, NS2_BTNR_R,		},
+	{ BTN_TL2,	1, NS2_BTNL_ZL,		},
+	{ BTN_TR2,	0, NS2_BTNR_ZR,		},
+	{ BTN_SELECT,	1, NS2_BTNL_MINUS,	},
+	{ BTN_START,	0, NS2_BTNR_PLUS,	},
+	{ BTN_THUMBL,	1, NS2_BTNL_LS,		},
+	{ BTN_THUMBR,	0, NS2_BTNR_RS,		},
+	{ BTN_MODE,	2, NS2_BTN_PRO_HOME	},
+	{ KEY_RECORD,	2, NS2_BTN_PRO_CAPTURE	},
+	{ BTN_GRIPR,	2, NS2_BTN_PRO_GR	},
+	{ BTN_GRIPL,	2, NS2_BTN_PRO_GL	},
+	{ BTN_C,	2, NS2_BTN_PRO_C	},
+	{ /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping gccon_mappings[] = {
+	{ BTN_SOUTH,	0, NS2_BTNR_A,		},
+	{ BTN_EAST,	0, NS2_BTNR_B,		},
+	{ BTN_NORTH,	0, NS2_BTNR_X,		},
+	{ BTN_WEST,	0, NS2_BTNR_Y,		},
+	{ BTN_TL,	1, NS2_BTNL_L,		},
+	{ BTN_TR,	0, NS2_BTNR_R,		},
+	{ BTN_TL2,	1, NS2_BTNL_ZL,		},
+	{ BTN_TR2,	0, NS2_BTNR_ZR,		},
+	{ BTN_SELECT,	1, NS2_BTNL_MINUS,	},
+	{ BTN_START,	0, NS2_BTNR_PLUS,	},
+	{ BTN_MODE,	2, NS2_BTN_GC_HOME	},
+	{ KEY_RECORD,	2, NS2_BTN_GC_CAPTURE	},
+	{ BTN_C,	2, NS2_BTN_GC_C		},
+	{ /* sentinel */ },
+};
+
+static const uint8_t switch2_init_cmd_data[] = {
+	/*
+	 * The last 6 bytes of this packet are the MAC address of
+	 * the console, but we don't need that for USB
+	 */
+	0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+};
+
+static const uint8_t switch2_one_data[] = { 0x01, 0x00, 0x00, 0x00 };
+#ifdef CONFIG_SWITCH2_FF
+static const uint8_t switch2_zero_data[] = { 0x00, 0x00, 0x00, 0x00 };
+#endif
+
+static const uint8_t switch2_player_pattern[] = { 0x1, 0x3, 0x7, 0xf, 0x9, 0x5, 0xd, 0x6 };
+
+static const uint8_t switch2_feature_mask[] = {
+	NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU | NS2_FEATURE_RUMBLE,
+	0x00, 0x00, 0x00
+};
+
+static DEFINE_IDA(switch2_player_id_allocator);
+
+#ifdef CONFIG_SWITCH2_FF
+static void switch2_encode_rumble(struct switch2_hd_rumble *rumble, uint8_t buffer[5])
+{
+	buffer[0] = rumble->hi_freq;
+	buffer[1] = (rumble->hi_freq >> 8) | (rumble->hi_amp << 2);
+	buffer[2] = (rumble->hi_amp >> 6) | (rumble->lo_freq << 4);
+	buffer[3] = (rumble->lo_freq >> 4) | (rumble->lo_amp << 6);
+	buffer[4] = rumble->lo_amp >> 2;
+}
+
+static int switch2_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
+{
+	struct switch2_controller *ns2 = input_get_drvdata(dev);
+
+	if (effect->type != FF_RUMBLE)
+		return 0;
+
+	guard(spinlock_irqsave)(&ns2->rumble_lock);
+	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+		ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
+			effect->u.rumble.weak_magnitude >> 1);
+	} else {
+		ns2->rumble.hd.hi_amp = effect->u.rumble.weak_magnitude * RUMBLE_MAX >> 16;
+		ns2->rumble.hd.lo_amp = effect->u.rumble.strong_magnitude * RUMBLE_MAX >> 16;
+	}
+
+	schedule_delayed_work(&ns2->rumble_work, 0);
+
+	return 0;
+}
+
+static void switch2_rumble_work(struct work_struct *work)
+{
+	struct switch2_controller *ns2 = container_of(to_delayed_work(work),
+						      struct switch2_controller, rumble_work);
+	unsigned long current_ms = jiffies_to_msecs(get_jiffies_64());
+	uint8_t *buffer = kzalloc(64, GFP_KERNEL);
+	unsigned long flags;
+	bool active;
+	int ret;
+
+	if (!buffer)
+		return;
+
+	spin_lock_irqsave(&ns2->rumble_lock, flags);
+	buffer[0x1] = 0x50 | ns2->rumble_seq;
+	if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+		buffer[0] = 3;
+		if (ns2->rumble.sd.amplitude == 0) {
+			buffer[2] = GC_RUMBLE_STOP;
+			ns2->rumble.sd.error = 0;
+			active = false;
+		} else {
+			if (ns2->rumble.sd.error < ns2->rumble.sd.amplitude) {
+				buffer[2] = GC_RUMBLE_ON;
+				ns2->rumble.sd.error += U16_MAX - ns2->rumble.sd.amplitude;
+			} else {
+				buffer[2] = GC_RUMBLE_OFF;
+				ns2->rumble.sd.error -= ns2->rumble.sd.amplitude;
+			}
+			active = true;
+		}
+	} else {
+		buffer[0] = 1;
+		switch2_encode_rumble(&ns2->rumble.hd, &buffer[0x2]);
+		active = ns2->rumble.hd.hi_amp || ns2->rumble.hd.lo_amp;
+		if (ns2->ctlr_type == NS2_CTLR_TYPE_PRO) {
+			/*
+			 * The Pro Controller contains separate LRAs on each
+			 * side that can be controlled individually.
+			 */
+			buffer[0] = 2;
+			buffer[0x11] = 0x50 | ns2->rumble_seq;
+			switch2_encode_rumble(&ns2->rumble.hd, &buffer[0x12]);
+		}
+	}
+	ns2->rumble_seq = (ns2->rumble_seq + 1) & 0xF;
+
+	if (active) {
+		unsigned long interval = msecs_to_jiffies(4);
+
+		if (!ns2->last_rumble_work)
+			ns2->last_rumble_work = current_ms;
+		else
+			ns2->last_rumble_work += interval;
+		schedule_delayed_work(&ns2->rumble_work,
+			ns2->last_rumble_work + interval - current_ms);
+	} else {
+		ns2->last_rumble_work = 0;
+	}
+	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+	if (!ns2->hdev) {
+		cancel_delayed_work(&ns2->rumble_work);
+		ret = -ENODEV;
+	} else {
+		ret = hid_hw_output_report(ns2->hdev, buffer, 64);
+	}
+
+	kfree(buffer);
+	if (ret < 0)
+		hid_dbg(ns2->hdev, "Failed to send output report ret=%d\n", ret);
+}
+#endif
+
+struct switch2_controller *switch2_get_controller(const char *phys)
+{
+	struct switch2_controller *ns2;
+
+	guard(mutex)(&switch2_controllers_lock);
+	list_for_each_entry(ns2, &switch2_controllers, entry) {
+		if (strncmp(ns2->phys, phys, sizeof(ns2->phys)) == 0)
+			return ns2;
+	}
+	ns2 = kzalloc(sizeof(*ns2), GFP_KERNEL);
+	if (!ns2)
+		return ERR_PTR(-ENOMEM);
+
+	mutex_init(&ns2->lock);
+	INIT_LIST_HEAD(&ns2->entry);
+	list_add(&ns2->entry, &switch2_controllers);
+	strscpy(ns2->phys, phys, sizeof(ns2->phys));
+	return ns2;
+}
+EXPORT_SYMBOL_GPL(switch2_get_controller);
+
+void switch2_controller_put(struct switch2_controller *ns2)
+{
+	mutex_lock(&ns2->lock);
+	if (ns2->input) {
+		input_unregister_device(ns2->input);
+		ns2->input = NULL;
+	}
+	ns2->init_step = 0;
+	if (ns2->hdev || ns2->cfg) {
+		mutex_unlock(&ns2->lock);
+		return;
+	}
+	mutex_unlock(&ns2->lock);
+	mutex_destroy(&ns2->lock);
+
+	mutex_lock(&switch2_controllers_lock);
+	list_del_init(&ns2->entry);
+	mutex_unlock(&switch2_controllers_lock);
+	kfree(ns2);
+}
+EXPORT_SYMBOL_GPL(switch2_controller_put);
+
+static bool switch2_parse_stick_calibration(struct switch2_stick_calibration *calib,
+	const uint8_t *data)
+{
+	static const uint8_t UNCALIBRATED[9] = {
+		0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+	};
+	if (memcmp(UNCALIBRATED, data, sizeof(UNCALIBRATED)) == 0)
+		return false;
+
+	calib->x.neutral = data[0];
+	calib->x.neutral |= (data[1] & 0x0F) << 8;
+
+	calib->y.neutral = data[1] >> 4;
+	calib->y.neutral |= data[2] << 4;
+
+	calib->x.positive = data[3];
+	calib->x.positive |= (data[4] & 0x0F) << 8;
+
+	calib->y.positive = data[4] >> 4;
+	calib->y.positive |= data[5] << 4;
+
+	calib->x.negative = data[6];
+	calib->x.negative |= (data[7] & 0x0F) << 8;
+
+	calib->y.negative = data[7] >> 4;
+	calib->y.negative |= data[8] << 4;
+
+	return true;
+}
+
+static void switch2_handle_flash_read(struct switch2_controller *ns2, uint8_t size,
+	uint32_t address, const uint8_t *data)
+{
+	bool ok;
+
+	switch (address) {
+	case NS2_FLASH_ADDR_SERIAL:
+		if (size != NS2_FLASH_SIZE_SERIAL)
+			return;
+		memcpy(ns2->serial, data, size);
+		break;
+	case NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB:
+		if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
+			return;
+		ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], data);
+		if (ok) {
+			hid_dbg(ns2->hdev, "Got factory primary stick calibration:\n");
+			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+				ns2->stick_calib[0].x.negative,
+				ns2->stick_calib[0].x.neutral,
+				ns2->stick_calib[0].x.positive);
+			hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+				ns2->stick_calib[0].y.negative,
+				ns2->stick_calib[0].y.neutral,
+				ns2->stick_calib[0].y.positive);
+		} else {
+			hid_dbg(ns2->hdev, "Factory primary stick calibration not present\n");
+		}
+		break;
+	case NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB:
+		if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
+			return;
+		ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], data);
+		if (ok) {
+			hid_dbg(ns2->hdev, "Got factory secondary stick calibration:\n");
+			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+				ns2->stick_calib[1].x.negative,
+				ns2->stick_calib[1].x.neutral,
+				ns2->stick_calib[1].x.positive);
+			hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+				ns2->stick_calib[1].y.negative,
+				ns2->stick_calib[1].y.neutral,
+				ns2->stick_calib[1].y.positive);
+		} else {
+			hid_dbg(ns2->hdev, "Factory secondary stick calibration not present\n");
+		}
+		break;
+	case NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB:
+		if (size != NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB)
+			return;
+		if (data[0] != 0xFF && data[1] != 0xFF) {
+			ns2->lt_zero = data[0];
+			ns2->rt_zero = data[1];
+
+			hid_dbg(ns2->hdev, "Got factory trigger calibration:\n");
+			hid_dbg(ns2->hdev, "Left zero point: %i\n", ns2->lt_zero);
+			hid_dbg(ns2->hdev, "Right zero point: %i\n", ns2->rt_zero);
+		} else {
+			hid_dbg(ns2->hdev, "Factory trigger calibration not present\n");
+		}
+		break;
+	case NS2_FLASH_ADDR_USER_PRIMARY_CALIB:
+		if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
+			return;
+		if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
+			hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
+			break;
+		}
+
+		ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], &data[2]);
+		if (ok) {
+			hid_dbg(ns2->hdev, "Got user primary stick calibration:\n");
+			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+				ns2->stick_calib[0].x.negative,
+				ns2->stick_calib[0].x.neutral,
+				ns2->stick_calib[0].x.positive);
+			hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+				ns2->stick_calib[0].y.negative,
+				ns2->stick_calib[0].y.neutral,
+				ns2->stick_calib[0].y.positive);
+		} else {
+			hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
+		}
+		break;
+	case NS2_FLASH_ADDR_USER_SECONDARY_CALIB:
+		if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
+			return;
+		if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
+			hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
+			break;
+		}
+
+		ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], &data[2]);
+		if (ok) {
+			hid_dbg(ns2->hdev, "Got user secondary stick calibration:\n");
+			hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+				ns2->stick_calib[1].x.negative,
+				ns2->stick_calib[1].x.neutral,
+				ns2->stick_calib[1].x.positive);
+			hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+				ns2->stick_calib[1].y.negative,
+				ns2->stick_calib[1].y.neutral,
+				ns2->stick_calib[1].y.positive);
+		} else {
+			hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
+		}
+		break;
+	}
+}
+
+int switch2_receive_command(struct switch2_controller *ns2,
+	const uint8_t *message, size_t length)
+{
+	const struct switch2_cmd_header *header;
+
+	if (length < 8)
+		return -EINVAL;
+
+	print_hex_dump_debug("got cmd: ", DUMP_PREFIX_OFFSET, 16, 1, message, length, false);
+
+	guard(mutex)(&ns2->lock);
+	if (ns2->init_step < NS2_INIT_DONE)
+		switch2_init_controller(ns2);
+
+	header = (const struct switch2_cmd_header *)message;
+	message = &message[8];
+	switch (header->command) {
+	case NS2_CMD_FLASH:
+		if (header->subcommand == NS2_SUBCMD_FLASH_READ) {
+			uint8_t read_size;
+			uint32_t read_address;
+
+			if (length < 16)
+				return -EINVAL;
+			read_size = message[0];
+			read_address = __le32_to_cpu(*(__le32 *)&message[4]);
+			if (length < read_size + 16)
+				return -EINVAL;
+			switch2_handle_flash_read(ns2, read_size, read_address, &message[8]);
+		}
+		break;
+	case NS2_CMD_FW_INFO:
+		if (header->subcommand == NS2_SUBCMD_FW_INFO_GET) {
+			if (length < sizeof(ns2->version))
+				return -EINVAL;
+			memcpy(&ns2->version, message, sizeof(ns2->version));
+			ns2->ctlr_type = ns2->version.ctlr_type;
+		}
+		break;
+	default:
+		break;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(switch2_receive_command);
+
+static int switch2_features_enable(struct switch2_controller *ns2, int features)
+{
+	__le32 feature_bits = __cpu_to_le32(features);
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+	return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_ENABLE,
+		&feature_bits, sizeof(feature_bits),
+		ns2->cfg);
+}
+
+static int switch2_read_flash(struct switch2_controller *ns2, uint32_t address,
+	uint8_t size)
+{
+	uint8_t message[8] = { size, 0x7e };
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+	*(__le32 *)&message[4] = __cpu_to_le32(address);
+	return ns2->cfg->send_command(NS2_CMD_FLASH, NS2_SUBCMD_FLASH_READ, message,
+		sizeof(message), ns2->cfg);
+}
+
+static int switch2_set_player_id(struct switch2_controller *ns2, uint32_t player_id)
+{
+	player_id %= NS2_MAX_PLAYER_ID;
+	uint8_t message[8] = { switch2_player_pattern[player_id] };
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+	return ns2->cfg->send_command(NS2_CMD_LED, NS2_SUBCMD_LED_PATTERN,
+		&message, sizeof(message),
+		ns2->cfg);
+}
+
+static void switch2_config_buttons(struct input_dev *idev,
+		 const struct switch2_ctlr_button_mapping button_mappings[])
+{
+	const struct switch2_ctlr_button_mapping *button;
+
+	for (button = button_mappings; button->code; button++)
+		input_set_capability(idev, EV_KEY, button->code);
+}
+
+static int switch2_init_input(struct switch2_controller *ns2)
+{
+	struct input_dev *input;
+	struct hid_device *hdev = ns2->hdev;
+#ifdef CONFIG_SWITCH2_FF
+	int ret;
+#endif
+
+	rcu_read_lock();
+	input = rcu_dereference(ns2->input);
+	rcu_read_unlock();
+
+	if (input)
+		return 0;
+
+	input = devm_input_allocate_device(&hdev->dev);
+	if (!input)
+		return -ENOMEM;
+
+	input_set_drvdata(input, ns2);
+	input->dev.parent = &hdev->dev;
+	input->id.bustype = hdev->bus;
+	input->id.vendor = hdev->vendor;
+	input->id.product = hdev->product;
+	input->id.version = hdev->version;
+	input->uniq = ns2->serial;
+	input->name = hdev->name;
+	input->phys = hdev->phys;
+
+	switch (ns2->ctlr_type) {
+	case NS2_CTLR_TYPE_JCL:
+		input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+		input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+		switch2_config_buttons(input, left_joycon_button_mappings);
+		break;
+	case NS2_CTLR_TYPE_JCR:
+		input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		switch2_config_buttons(input, right_joycon_button_mappings);
+		break;
+	case NS2_CTLR_TYPE_GC:
+		input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Z, 0, NS2_TRIGGER_RANGE, 32, 128);
+		input_set_abs_params(input, ABS_RZ, 0, NS2_TRIGGER_RANGE, 32, 128);
+		input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+		input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+		switch2_config_buttons(input, gccon_mappings);
+		break;
+	case NS2_CTLR_TYPE_PRO:
+		input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+		input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+		input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+		switch2_config_buttons(input, procon_mappings);
+		break;
+	default:
+		input_free_device(input);
+		return -EINVAL;
+	}
+
+#ifdef CONFIG_SWITCH2_FF
+	input_set_capability(input, EV_FF, FF_RUMBLE);
+	ret = input_ff_create_memless(input, NULL, switch2_play_effect);
+	if (ret) {
+		input_free_device(input);
+		return ret;
+	}
+#endif
+
+	rcu_assign_pointer(ns2->input, input);
+
+	hid_info(ns2->hdev, "Firmware version %u.%u.%u (%i)\n", ns2->version.major,
+		ns2->version.minor, ns2->version.patch, ns2->version.ctlr_type);
+	if (ns2->version.dsp_type >= 0)
+		hid_info(ns2->hdev, "DSP version %u.%u.%u\n", ns2->version.dsp_major,
+			ns2->version.dsp_minor, ns2->version.dsp_patch);
+	return input_register_device(input);
+}
+
+int switch2_init_controller(struct switch2_controller *ns2)
+{
+	if (ns2->init_step == NS2_INIT_DONE)
+		return 0;
+
+	if (!ns2->cfg)
+		return -ENOTCONN;
+
+	while (ns2->init_step < NS2_INIT_DONE) {
+		ns2->init_step++;
+		/* TODO verify that the step completed successfully */
+		switch (ns2->init_step) {
+		case NS2_INIT_READ_SERIAL:
+			return switch2_read_flash(ns2, NS2_FLASH_ADDR_SERIAL,
+				NS2_FLASH_SIZE_SERIAL);
+		case NS2_INIT_GET_FIRMWARE_INFO:
+			return ns2->cfg->send_command(NS2_CMD_FW_INFO, NS2_SUBCMD_FW_INFO_GET,
+				NULL, 0, ns2->cfg);
+			break;
+		case NS2_INIT_READ_FACTORY_PRIMARY_CALIB:
+			return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB,
+				NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
+		case NS2_INIT_READ_FACTORY_SECONDARY_CALIB:
+			if (switch2_ctlr_is_joycon(ns2->ctlr_type))
+				break;
+			return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB,
+				NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
+		case NS2_INIT_READ_FACTORY_TRIGGER_CALIB:
+			if (ns2->ctlr_type != NS2_CTLR_TYPE_GC)
+				break;
+			return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB,
+				NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB);
+		case NS2_INIT_READ_USER_PRIMARY_CALIB:
+			return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_PRIMARY_CALIB,
+				NS2_FLASH_SIZE_USER_AXIS_CALIB);
+		case NS2_INIT_READ_USER_SECONDARY_CALIB:
+			if (switch2_ctlr_is_joycon(ns2->ctlr_type))
+				break;
+			return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_SECONDARY_CALIB,
+				NS2_FLASH_SIZE_USER_AXIS_CALIB);
+		case NS2_INIT_SET_FEATURE_MASK:
+			return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_SET_MASK,
+				switch2_feature_mask, sizeof(switch2_feature_mask), ns2->cfg);
+			break;
+		case NS2_INIT_ENABLE_FEATURES:
+			return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS |
+				NS2_FEATURE_ANALOG | NS2_FEATURE_RUMBLE);
+			break;
+#ifdef CONFIG_SWITCH2_FF
+		case NS2_INIT_ENABLE_RUMBLE:
+			return ns2->cfg->send_command(NS2_CMD_NFC, 1,
+				switch2_zero_data, sizeof(switch2_zero_data),
+				ns2->cfg);
+			break;
+#endif
+		case NS2_INIT_GRIP_BUTTONS:
+			if (!switch2_ctlr_is_joycon(ns2->ctlr_type))
+				break;
+			return ns2->cfg->send_command(NS2_CMD_GRIP, NS2_SUBCMD_GRIP_ENABLE_BUTTONS,
+				switch2_one_data, sizeof(switch2_one_data),
+				ns2->cfg);
+			break;
+		case NS2_INIT_SET_PLAYER_LEDS:
+			return switch2_set_player_id(ns2, ns2->player_id);
+		case NS2_INIT_INPUT:
+			return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_USB,
+				switch2_init_cmd_data, sizeof(switch2_init_cmd_data), ns2->cfg);
+		case NS2_INIT_DONE:
+			if (ns2->hdev)
+				return switch2_init_input(ns2);
+			break;
+		default:
+			WARN_ON_ONCE(1);
+		}
+	}
+	return 0;
+}
+EXPORT_SYMBOL_GPL(switch2_init_controller);
+
+static void switch2_report_buttons(struct input_dev *input, const uint8_t *bytes,
+	const struct switch2_ctlr_button_mapping button_mappings[])
+{
+	const struct switch2_ctlr_button_mapping *button;
+
+	for (button = button_mappings; button->code; button++)
+		input_report_key(input, button->code, bytes[button->byte] & button->bit);
+}
+
+static void switch2_report_axis(struct input_dev *input, struct switch2_axis_calibration *calib,
+	int axis, int value, bool negate)
+{
+	if (calib && calib->neutral && calib->negative && calib->positive) {
+		value -= calib->neutral;
+		value *= NS2_AXIS_MAX + 1;
+		if (value < 0)
+			value /= calib->negative;
+		else
+			value /= calib->positive;
+	} else {
+		value = (value - 2048) * 16;
+	}
+
+	if (negate)
+		value = -value;
+	input_report_abs(input, axis,
+		clamp(value, NS2_AXIS_MIN, NS2_AXIS_MAX));
+}
+
+static void switch2_report_stick(struct input_dev *input, struct switch2_stick_calibration *calib,
+	int x, int y, const uint8_t *data)
+{
+	switch2_report_axis(input, &calib->x, x, data[0] | ((data[1] & 0x0F) << 8), false);
+	switch2_report_axis(input, &calib->y, y, (data[1] >> 4) | (data[2] << 4), true);
+}
+
+static void switch2_report_trigger(struct input_dev *input, uint8_t zero, int abs, uint8_t data)
+{
+	int value = (NS2_TRIGGER_RANGE + 1) * (data - zero) / (232 - zero);
+
+	input_report_abs(input, abs, clamp(value, 0, NS2_TRIGGER_RANGE));
+}
+
+static int switch2_event(struct hid_device *hdev, struct hid_report *report, uint8_t *raw_data,
+	int size)
+{
+	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+	struct input_dev *input;
+
+	if (report->type != HID_INPUT_REPORT)
+		return 0;
+
+	if (size < 15)
+		return -EINVAL;
+
+	rcu_read_lock();
+	input = rcu_dereference(ns2->input);
+	rcu_read_unlock();
+
+	if (!input)
+		return 0;
+
+	switch (report->id) {
+	case NS2_REPORT_UNIFIED:
+		/*
+		 * TODO
+		 * This won't be sent unless the report type gets changed via command
+		 * 03-0A, but we should support it at some point regardless.
+		 */
+		break;
+	case NS2_REPORT_JCL:
+		input_report_abs(input, ABS_HAT0X,
+			!!(raw_data[3] & NS2_BTNL_RIGHT) -
+			!!(raw_data[3] & NS2_BTNL_LEFT));
+		input_report_abs(input, ABS_HAT0Y,
+			!!(raw_data[3] & NS2_BTNL_DOWN) -
+			!!(raw_data[3] & NS2_BTNL_UP));
+		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, ABS_Y, &raw_data[6]);
+		switch2_report_buttons(input, &raw_data[3], left_joycon_button_mappings);
+		break;
+	case NS2_REPORT_JCR:
+		switch2_report_stick(input, &ns2->stick_calib[0], ABS_RX, ABS_RY, &raw_data[6]);
+		switch2_report_buttons(input, &raw_data[3], right_joycon_button_mappings);
+		break;
+	case NS2_REPORT_GC:
+		input_report_abs(input, ABS_HAT0X,
+			!!(raw_data[4] & NS2_BTNL_RIGHT) -
+			!!(raw_data[4] & NS2_BTNL_LEFT));
+		input_report_abs(input, ABS_HAT0Y,
+			!!(raw_data[4] & NS2_BTNL_DOWN) -
+			!!(raw_data[4] & NS2_BTNL_UP));
+		switch2_report_buttons(input, &raw_data[3], gccon_mappings);
+		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, ABS_Y, &raw_data[6]);
+		switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, ABS_RY, &raw_data[9]);
+		switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[13]);
+		switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[14]);
+		break;
+	case NS2_REPORT_PRO:
+		input_report_abs(input, ABS_HAT0X,
+			!!(raw_data[4] & NS2_BTNL_RIGHT) -
+			!!(raw_data[4] & NS2_BTNL_LEFT));
+		input_report_abs(input, ABS_HAT0Y,
+			!!(raw_data[4] & NS2_BTNL_DOWN) -
+			!!(raw_data[4] & NS2_BTNL_UP));
+		switch2_report_buttons(input, &raw_data[3], procon_mappings);
+		switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, ABS_Y, &raw_data[6]);
+		switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, ABS_RY, &raw_data[9]);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	input_sync(input);
+	return 0;
+}
+
+static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	struct switch2_controller *ns2;
+	struct usb_device *udev;
+	char phys[64];
+	int ret;
+
+	if (!hid_is_usb(hdev))
+		return -ENODEV;
+
+	udev = hid_to_usb_dev(hdev);
+	if (usb_make_path(udev, phys, sizeof(phys)) < 0)
+		return -EINVAL;
+
+	ret = hid_parse(hdev);
+	if (ret) {
+		hid_err(hdev, "parse failed %d\n", ret);
+		return ret;
+	}
+
+	ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+	if (ret) {
+		hid_err(hdev, "hw_start failed %d\n", ret);
+		return ret;
+	}
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_err(hdev, "hw_open failed %d\n", ret);
+		goto err_stop;
+	}
+
+	ns2 = switch2_get_controller(phys);
+	if (!ns2) {
+		ret = -ENOMEM;
+		goto err_close;
+	}
+
+	guard(mutex)(&ns2->lock);
+	ns2->hdev = hdev;
+
+	ns2->player_id = U32_MAX;
+	ret = ida_alloc(&switch2_player_id_allocator, GFP_KERNEL);
+	if (ret < 0)
+		hid_warn(hdev, "Failed to allocate player ID, skipping; ret=%d\n", ret);
+	else
+		ns2->player_id = ret;
+#ifdef CONFIG_SWITCH2_FF
+	if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) {
+		ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
+		ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
+	}
+	spin_lock_init(&ns2->rumble_lock);
+	INIT_DELAYED_WORK(&ns2->rumble_work, switch2_rumble_work);
+#endif
+	hid_set_drvdata(hdev, ns2);
+
+	if (ns2->cfg)
+		return switch2_init_controller(ns2);
+
+	return 0;
+
+err_close:
+	hid_hw_close(hdev);
+err_stop:
+	hid_hw_stop(hdev);
+
+	return ret;
+}
+
+static void switch2_remove(struct hid_device *hdev)
+{
+	struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+#ifdef CONFIG_SWITCH2_FF
+	unsigned long flags;
+#endif
+
+#ifdef CONFIG_SWITCH2_FF
+	spin_lock_irqsave(&ns2->rumble_lock, flags);
+	cancel_delayed_work_sync(&ns2->rumble_work);
+	spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+#endif
+	mutex_lock(&ns2->lock);
+	ns2->hdev = NULL;
+	mutex_unlock(&ns2->lock);
+	hid_hw_close(hdev);
+	ida_free(&switch2_player_id_allocator, ns2->player_id);
+	switch2_controller_put(ns2);
+	hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id switch2_devices[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
+	{}
+};
+MODULE_DEVICE_TABLE(hid, switch2_devices);
+
+static struct hid_driver switch2_hid_driver = {
+	.name		= "switch2",
+	.id_table	= switch2_devices,
+	.probe		= switch2_probe,
+	.remove		= switch2_remove,
+	.raw_event	= switch2_event,
+};
+
+static int __init switch2_init(void)
+{
+	return hid_register_driver(&switch2_hid_driver);
+}
+
+static void __exit switch2_exit(void)
+{
+	hid_unregister_driver(&switch2_hid_driver);
+	ida_destroy(&switch2_player_id_allocator);
+}
+
+module_init(switch2_init);
+module_exit(switch2_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
+MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers");
diff --git a/drivers/hid/hid-switch2.h b/drivers/hid/hid-switch2.h
new file mode 100644
index 000000000000..561b16bbe3be
--- /dev/null
+++ b/drivers/hid/hid-switch2.h
@@ -0,0 +1,242 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * HID driver for Nintendo Switch controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the following work:
+ *   https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ *   https://github.com/ndeadly/switch2_controller_research
+ */
+
+#include <linux/bits.h>
+#include <linux/input.h>
+#include <linux/mutex.h>
+#include <linux/rcupdate.h>
+#include <linux/spinlock.h>
+#include "hid-ids.h"
+
+#define NS2_FLAG_OK	BIT(0)
+#define NS2_FLAG_NACK	BIT(2)
+
+#define NS2_FLASH_ADDR_SERIAL			0x13002
+#define NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB	0x130a8
+#define NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB	0x130e8
+#define NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB	0x13140
+#define NS2_FLASH_ADDR_USER_PRIMARY_CALIB	0x1fc040
+#define NS2_FLASH_ADDR_USER_SECONDARY_CALIB	0x1fc080
+
+#define NS2_FLASH_SIZE_SERIAL 0x10
+#define NS2_FLASH_SIZE_FACTORY_AXIS_CALIB 9
+#define NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB 2
+#define NS2_FLASH_SIZE_USER_AXIS_CALIB 11
+
+#define NS2_USER_CALIB_MAGIC 0xa1b2
+
+#define NS2_FEATURE_BUTTONS	BIT(0)
+#define NS2_FEATURE_ANALOG	BIT(1)
+#define NS2_FEATURE_IMU		BIT(2)
+#define NS2_FEATURE_MOUSE	BIT(4)
+#define NS2_FEATURE_RUMBLE	BIT(5)
+#define NS2_FEATURE_MAGNETO	BIT(7)
+
+enum switch2_cmd {
+	NS2_CMD_NFC = 0x01,
+	NS2_CMD_FLASH = 0x02,
+	NS2_CMD_INIT = 0x03,
+	NS2_CMD_GRIP = 0x08,
+	NS2_CMD_LED = 0x09,
+	NS2_CMD_VIBRATE = 0x0a,
+	NS2_CMD_BATTERY = 0x0b,
+	NS2_CMD_FEATSEL = 0x0c,
+	NS2_CMD_FW_UPD = 0x0d,
+	NS2_CMD_FW_INFO = 0x10,
+	NS2_CMD_BT_PAIR = 0x15,
+};
+
+enum switch2_direction {
+	NS2_DIR_IN = 0x00,
+	NS2_DIR_OUT = 0x90,
+};
+
+enum switch2_transport {
+	NS2_TRANS_USB = 0x00,
+	NS2_TRANS_BT = 0x01,
+};
+
+enum switch2_subcmd_flash {
+	NS2_SUBCMD_FLASH_READ_BLOCK = 0x01,
+	NS2_SUBCMD_FLASH_WRITE_BLOCK = 0x02,
+	NS2_SUBCMD_FLASH_ERASE_BLOCK = 0x03,
+	NS2_SUBCMD_FLASH_READ = 0x04,
+	NS2_SUBCMD_FLASH_WRITE = 0x05,
+};
+
+enum switch2_subcmd_init {
+	NS2_SUBCMD_INIT_SELECT_REPORT = 0xa,
+	NS2_SUBCMD_INIT_USB = 0xd,
+};
+
+enum switch2_subcmd_feature_select {
+	NS2_SUBCMD_FEATSEL_GET_INFO = 0x1,
+	NS2_SUBCMD_FEATSEL_SET_MASK = 0x2,
+	NS2_SUBCMD_FEATSEL_CLEAR_MASK = 0x3,
+	NS2_SUBCMD_FEATSEL_ENABLE = 0x4,
+	NS2_SUBCMD_FEATSEL_DISABLE = 0x5,
+};
+
+enum switch2_subcmd_grip {
+	NS2_SUBCMD_GRIP_GET_INFO = 0x1,
+	NS2_SUBCMD_GRIP_ENABLE_BUTTONS = 0x2,
+	NS2_SUBCMD_GRIP_GET_INFO_EXT = 0x3,
+};
+
+enum switch2_subcmd_led {
+	NS2_SUBCMD_LED_P1 = 0x1,
+	NS2_SUBCMD_LED_P2 = 0x2,
+	NS2_SUBCMD_LED_P3 = 0x3,
+	NS2_SUBCMD_LED_P4 = 0x4,
+	NS2_SUBCMD_LED_ALL_ON = 0x5,
+	NS2_SUBCMD_LED_ALL_OFF = 0x6,
+	NS2_SUBCMD_LED_PATTERN = 0x7,
+	NS2_SUBCMD_LED_BLINK = 0x8,
+};
+
+enum switch2_subcmd_fw_info {
+	NS2_SUBCMD_FW_INFO_GET = 0x1,
+};
+
+enum switch2_ctlr_type {
+	NS2_CTLR_TYPE_JCL = 0x00,
+	NS2_CTLR_TYPE_JCR = 0x01,
+	NS2_CTLR_TYPE_PRO = 0x02,
+	NS2_CTLR_TYPE_GC = 0x03,
+};
+
+enum switch2_report_id {
+	NS2_REPORT_UNIFIED = 0x05,
+	NS2_REPORT_JCL = 0x07,
+	NS2_REPORT_JCR = 0x08,
+	NS2_REPORT_PRO = 0x09,
+	NS2_REPORT_GC = 0x0a,
+};
+
+enum switch2_init_step {
+	NS2_INIT_STARTING,
+	NS2_INIT_READ_SERIAL,
+	NS2_INIT_READ_FACTORY_PRIMARY_CALIB,
+	NS2_INIT_READ_FACTORY_SECONDARY_CALIB,
+	NS2_INIT_READ_FACTORY_TRIGGER_CALIB,
+	NS2_INIT_READ_USER_PRIMARY_CALIB,
+	NS2_INIT_READ_USER_SECONDARY_CALIB,
+	NS2_INIT_SET_FEATURE_MASK,
+	NS2_INIT_ENABLE_FEATURES,
+	NS2_INIT_GET_FIRMWARE_INFO,
+#ifdef CONFIG_SWITCH2_FF
+	NS2_INIT_ENABLE_RUMBLE,
+#endif
+	NS2_INIT_GRIP_BUTTONS,
+	NS2_INIT_SET_PLAYER_LEDS,
+	NS2_INIT_INPUT,
+	NS2_INIT_DONE,
+};
+
+struct switch2_cmd_header {
+	uint8_t command;
+	uint8_t direciton;
+	uint8_t transport;
+	uint8_t subcommand;
+	uint8_t unk1;
+	uint8_t length;
+	uint16_t unk2;
+};
+static_assert(sizeof(struct switch2_cmd_header) == 8);
+
+struct switch2_controller;
+struct switch2_cfg_intf {
+	struct switch2_controller *parent;
+
+	int (*send_command)(enum switch2_cmd command, uint8_t subcommand,
+		const void *message, size_t length,
+		struct switch2_cfg_intf *intf);
+};
+
+struct switch2_version_info {
+	uint8_t major;
+	uint8_t minor;
+	uint8_t patch;
+	uint8_t ctlr_type;
+	__le32 unk;
+	int8_t dsp_major;
+	int8_t dsp_minor;
+	int8_t dsp_patch;
+	int8_t dsp_type;
+};
+
+struct switch2_axis_calibration {
+	uint16_t neutral;
+	uint16_t negative;
+	uint16_t positive;
+};
+
+struct switch2_stick_calibration {
+	struct switch2_axis_calibration x;
+	struct switch2_axis_calibration y;
+};
+
+struct switch2_hd_rumble {
+	uint16_t hi_freq : 10;
+	uint16_t hi_amp : 10;
+	uint16_t lo_freq : 10;
+	uint16_t lo_amp : 10;
+} __packed;
+
+struct switch2_erm_rumble {
+	uint16_t error;
+	uint16_t amplitude;
+};
+
+struct switch2_controller {
+	struct hid_device *hdev;
+	struct switch2_cfg_intf *cfg;
+
+	char phys[64];
+	struct list_head entry;
+	struct mutex lock;
+
+	enum switch2_ctlr_type ctlr_type;
+	enum switch2_init_step init_step;
+	struct input_dev __rcu *input;
+	char serial[NS2_FLASH_SIZE_SERIAL + 1];
+	struct switch2_version_info version;
+
+	struct switch2_stick_calibration stick_calib[2];
+	uint8_t lt_zero;
+	uint8_t rt_zero;
+
+	uint32_t player_id;
+
+#ifdef CONFIG_SWITCH2_FF
+	spinlock_t rumble_lock;
+	uint8_t rumble_seq;
+	union {
+		struct switch2_hd_rumble hd;
+		struct switch2_erm_rumble sd;
+	} rumble;
+	unsigned long last_rumble_work;
+	struct delayed_work rumble_work;
+#endif
+};
+
+struct switch2_controller *switch2_get_controller(const char *phys);
+void switch2_controller_put(struct switch2_controller *controller);
+
+int switch2_receive_command(struct switch2_controller *controller,
+	const uint8_t *message, size_t length);
+
+int switch2_init_controller(struct switch2_controller *controller);
+
+static inline bool switch2_ctlr_is_joycon(enum switch2_ctlr_type type)
+{
+	return type == NS2_CTLR_TYPE_JCL || type == NS2_CTLR_TYPE_JCR;
+}
diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
index 7755e5b454d2..73b0e15205ae 100644
--- a/drivers/input/joystick/Kconfig
+++ b/drivers/input/joystick/Kconfig
@@ -422,4 +422,12 @@ config JOYSTICK_SEESAW
 	  To compile this driver as a module, choose M here: the module will be
 	  called adafruit-seesaw.
 
+config JOYSTICK_SWITCH2_USB
+	tristate "Wired Nintendo Switch 2 controller support"
+	depends on HID_SWITCH2
+	select USB
+	help
+	  To compile this driver as a module, choose M here: the
+	  module will be called switch2-usb.
+
 endif
diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
index 9976f596a920..46feb7ef60ba 100644
--- a/drivers/input/joystick/Makefile
+++ b/drivers/input/joystick/Makefile
@@ -34,6 +34,7 @@ obj-$(CONFIG_JOYSTICK_SIDEWINDER)	+= sidewinder.o
 obj-$(CONFIG_JOYSTICK_SPACEBALL)	+= spaceball.o
 obj-$(CONFIG_JOYSTICK_SPACEORB)		+= spaceorb.o
 obj-$(CONFIG_JOYSTICK_STINGER)		+= stinger.o
+obj-$(CONFIG_JOYSTICK_SWITCH2_USB)	+= switch2-usb.o
 obj-$(CONFIG_JOYSTICK_TMDC)		+= tmdc.o
 obj-$(CONFIG_JOYSTICK_TURBOGRAFX)	+= turbografx.o
 obj-$(CONFIG_JOYSTICK_TWIDJOY)		+= twidjoy.o
diff --git a/drivers/input/joystick/switch2-usb.c b/drivers/input/joystick/switch2-usb.c
new file mode 100644
index 000000000000..c16047f068ee
--- /dev/null
+++ b/drivers/input/joystick/switch2-usb.c
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * HID driver for Nintendo Switch controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the following work:
+ *   https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ *   https://github.com/ndeadly/switch2_controller_research
+ */
+
+#include "../../hid/hid-switch2.h"
+#include <linux/module.h>
+#include <linux/usb/input.h>
+
+#define NS2_BULK_SIZE 64
+#define NS2_IN_URBS 2
+#define NS2_OUT_URBS 4
+
+static struct usb_driver switch2_usb;
+
+struct switch2_urb {
+	struct urb *urb;
+	uint8_t *data;
+	bool active;
+};
+
+struct switch2_usb {
+	struct switch2_cfg_intf cfg;
+	struct usb_device *udev;
+
+	struct switch2_urb bulk_in[NS2_IN_URBS];
+	struct usb_anchor bulk_in_anchor;
+	spinlock_t bulk_in_lock;
+
+	struct switch2_urb bulk_out[NS2_OUT_URBS];
+	struct usb_anchor bulk_out_anchor;
+	spinlock_t bulk_out_lock;
+
+	int message_in;
+	struct work_struct message_in_work;
+};
+
+static void switch2_bulk_in(struct urb *urb)
+{
+	struct switch2_usb *ns2_usb = urb->context;
+	int i;
+	bool schedule = false;
+	unsigned long flags;
+
+	switch (urb->status) {
+	case 0:
+		schedule = true;
+		break;
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		return;
+	default:
+		dev_dbg(&ns2_usb->udev->dev, "unknown urb status: %d\n",
+			urb->status);
+		break;
+	}
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		int err;
+		struct switch2_urb *ns2_urb;
+
+		if (ns2_usb->bulk_in[i].urb == urb) {
+			ns2_usb->message_in = i;
+			continue;
+		}
+
+		if (ns2_usb->bulk_in[i].active)
+			continue;
+
+		ns2_urb = &ns2_usb->bulk_in[i];
+		usb_anchor_urb(ns2_urb->urb, &ns2_usb->bulk_out_anchor);
+		err = usb_submit_urb(ns2_urb->urb, GFP_ATOMIC);
+		if (err) {
+			usb_unanchor_urb(ns2_urb->urb);
+			dev_dbg(&ns2_usb->udev->dev, "failed to queue input urb: %d\n", err);
+		} else {
+			ns2_urb->active = true;
+		}
+	}
+	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+	if (schedule)
+		schedule_work(&ns2_usb->message_in_work);
+}
+
+static void switch2_bulk_out(struct urb *urb)
+{
+	struct switch2_usb *ns2_usb = urb->context;
+	int i;
+
+	guard(spinlock_irqsave)(&ns2_usb->bulk_out_lock);
+
+	switch (urb->status) {
+	case 0:
+		break;
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		return;
+	default:
+		dev_dbg(&ns2_usb->udev->dev, "unknown urb status: %d\n", urb->status);
+		return;
+	}
+
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		if (ns2_usb->bulk_out[i].urb != urb)
+			continue;
+
+		ns2_usb->bulk_out[i].active = false;
+		break;
+	}
+}
+
+static int switch2_usb_send_cmd(enum switch2_cmd command, uint8_t subcommand,
+	const void *message, size_t size, struct switch2_cfg_intf *cfg)
+{
+	struct switch2_usb *ns2_usb = (struct switch2_usb *)cfg;
+	struct switch2_urb *urb = NULL;
+	int i;
+	int ret;
+
+	struct switch2_cmd_header header = {
+		command, NS2_DIR_OUT | NS2_FLAG_OK, NS2_TRANS_USB, subcommand, 0, size
+	};
+
+	if (WARN_ON(size > 56))
+		return -EINVAL;
+
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		if (ns2_usb->bulk_out[i].active)
+			continue;
+
+		urb = &ns2_usb->bulk_out[i];
+		urb->active = true;
+		break;
+	}
+	if (!urb) {
+		dev_warn(&ns2_usb->udev->dev, "output queue full, dropping message\n");
+		return -ENOBUFS;
+	}
+
+	memcpy(urb->data, &header, sizeof(header));
+	if (message && size)
+		memcpy(&urb->data[8], message, size);
+	urb->urb->transfer_buffer_length = size + sizeof(header);
+
+	print_hex_dump_debug("sending cmd: ", DUMP_PREFIX_OFFSET, 16, 1, urb->data,
+		size + sizeof(header), false);
+
+	usb_anchor_urb(urb->urb, &ns2_usb->bulk_out_anchor);
+	ret = usb_submit_urb(urb->urb, GFP_ATOMIC);
+	if (ret) {
+		dev_warn(&ns2_usb->udev->dev, "failed to submit urb: %i", ret);
+		urb->active = false;
+		usb_unanchor_urb(urb->urb);
+		return ret;
+	}
+
+	return 0;
+}
+
+static void switch2_usb_message_in_work(struct work_struct *work)
+{
+	struct switch2_usb *ns2_usb = container_of(work, struct switch2_usb, message_in_work);
+	struct switch2_urb *urb;
+	int err;
+	unsigned long flags;
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	urb = &ns2_usb->bulk_in[ns2_usb->message_in];
+	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+	err = switch2_receive_command(ns2_usb->cfg.parent, urb->urb->transfer_buffer,
+		urb->urb->actual_length);
+	if (err)
+		dev_dbg(&ns2_usb->udev->dev, "receive command failed: %d\n", err);
+
+	spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+	urb->active = false;
+	spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+}
+
+static int switch2_usb_probe(struct usb_interface *intf, const struct usb_device_id *id)
+{
+	struct switch2_controller *ns2;
+	struct switch2_usb *ns2_usb;
+	struct usb_device *udev;
+	struct usb_endpoint_descriptor *bulk_in, *bulk_out;
+	char phys[64];
+	int ret;
+	int i;
+
+	udev = interface_to_usbdev(intf);
+	if (usb_make_path(udev, phys, sizeof(phys)) < 0)
+		return -EINVAL;
+
+	ret = usb_find_common_endpoints(intf->cur_altsetting, &bulk_in, &bulk_out, NULL, NULL);
+	if (ret) {
+		dev_err(&intf->dev, "failed to find bulk EPs\n");
+		return ret;
+	}
+
+	ns2_usb = devm_kzalloc(&intf->dev, sizeof(*ns2_usb), GFP_KERNEL);
+	if (!ns2_usb)
+		return -ENOMEM;
+
+	ns2_usb->udev = udev;
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		ns2_usb->bulk_in[i].urb = usb_alloc_urb(0, GFP_KERNEL);
+		if (!ns2_usb->bulk_in[i].urb) {
+			ret = -ENOMEM;
+			goto err_free_in;
+		}
+
+		ns2_usb->bulk_in[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
+			&ns2_usb->bulk_in[i].urb->transfer_dma);
+		if (!ns2_usb->bulk_in[i].data) {
+			ret = -ENOMEM;
+			goto err_free_in;
+		}
+
+		usb_fill_bulk_urb(ns2_usb->bulk_in[i].urb, udev,
+			usb_rcvbulkpipe(udev, bulk_in->bEndpointAddress),
+			ns2_usb->bulk_in[i].data, NS2_BULK_SIZE, switch2_bulk_in, ns2_usb);
+		ns2_usb->bulk_in[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+	}
+
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		ns2_usb->bulk_out[i].urb = usb_alloc_urb(0, GFP_KERNEL);
+		if (!ns2_usb->bulk_out[i].urb) {
+			ret = -ENOMEM;
+			goto err_free_out;
+		}
+
+		ns2_usb->bulk_out[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
+			&ns2_usb->bulk_out[i].urb->transfer_dma);
+		if (!ns2_usb->bulk_out[i].data) {
+			ret = -ENOMEM;
+			goto err_free_out;
+		}
+
+		usb_fill_bulk_urb(ns2_usb->bulk_out[i].urb, udev,
+			usb_sndbulkpipe(udev, bulk_out->bEndpointAddress),
+			ns2_usb->bulk_out[i].data, NS2_BULK_SIZE, switch2_bulk_out, ns2_usb);
+		ns2_usb->bulk_out[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+	}
+
+	ns2_usb->bulk_in[0].active = true;
+	ret = usb_submit_urb(ns2_usb->bulk_in[0].urb, GFP_ATOMIC);
+	if (ret < 0)
+		goto err_free_out;
+
+	ns2 = switch2_get_controller(phys);
+	if (IS_ERR(ns2)) {
+		ret = PTR_ERR(ns2);
+		goto err_kill_urb;
+	}
+
+	ns2_usb->cfg.parent = ns2;
+	ns2_usb->cfg.send_command = switch2_usb_send_cmd;
+
+	init_usb_anchor(&ns2_usb->bulk_out_anchor);
+	spin_lock_init(&ns2_usb->bulk_out_lock);
+	init_usb_anchor(&ns2_usb->bulk_in_anchor);
+	spin_lock_init(&ns2_usb->bulk_in_lock);
+	INIT_WORK(&ns2_usb->message_in_work, switch2_usb_message_in_work);
+
+	usb_set_intfdata(intf, ns2_usb);
+
+	guard(mutex)(&ns2->lock);
+	ns2->cfg = (struct switch2_cfg_intf *) ns2_usb;
+
+	if (ns2->hdev)
+		return switch2_init_controller(ns2);
+
+	return 0;
+
+err_kill_urb:
+	usb_kill_urb(ns2_usb->bulk_in[0].urb);
+err_free_out:
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
+			ns2_usb->bulk_out[i].urb->transfer_dma);
+		usb_free_urb(ns2_usb->bulk_out[i].urb);
+	}
+err_free_in:
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
+			ns2_usb->bulk_in[i].urb->transfer_dma);
+		usb_free_urb(ns2_usb->bulk_in[i].urb);
+	}
+	devm_kfree(&intf->dev, ns2_usb);
+
+	return ret;
+}
+
+static void switch2_usb_disconnect(struct usb_interface *intf)
+{
+	struct switch2_usb *ns2_usb = usb_get_intfdata(intf);
+	struct switch2_controller *ns2 = ns2_usb->cfg.parent;
+	int i;
+
+	usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
+	for (i = 0; i < NS2_OUT_URBS; i++) {
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
+			ns2_usb->bulk_out[i].urb->transfer_dma);
+		usb_free_urb(ns2_usb->bulk_out[i].urb);
+	}
+
+	usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
+	for (i = 0; i < NS2_IN_URBS; i++) {
+		usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
+			ns2_usb->bulk_in[i].urb->transfer_dma);
+		usb_free_urb(ns2_usb->bulk_in[i].urb);
+	}
+
+	mutex_lock(&ns2->lock);
+	ns2->cfg = NULL;
+	mutex_unlock(&ns2->lock);
+	switch2_controller_put(ns2);
+}
+
+#define SWITCH2_CONTROLLER(vend, prod) \
+	USB_DEVICE_AND_INTERFACE_INFO(vend, prod, USB_CLASS_VENDOR_SPEC, 0, 0)
+
+static const struct usb_device_id switch2_usb_devices[] = {
+	{ SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
+	{ SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
+	{ SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
+	{ SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
+	{ }
+};
+MODULE_DEVICE_TABLE(usb, switch2_usb_devices);
+
+static struct usb_driver switch2_usb = {
+	.name		= "switch2",
+	.id_table	= switch2_usb_devices,
+	.probe		= switch2_usb_probe,
+	.disconnect	= switch2_usb_disconnect,
+};
+module_usb_driver(switch2_usb);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
+MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers");
-- 
2.51.2


^ permalink raw reply related

* [PATCH v2 0/1] HID: switch2: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2026-01-09  3:40 UTC (permalink / raw)
  To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
  Cc: Vicki Pfau, Luiz Augusto von Dentz, Bastien Nocera

This is effectively a resubmission of the previous patch with minor fixes
for issues that came up during the first review. Bluetooth support is still
not included, but this will hopefully provide a solid foundation for
bringing up Bluetooth support once BlueZ is capable of bringing up the
controller. This will probably require a new plugin, but investigation is
still ongoing.

Vicki Pfau (1):
  HID: switch2: Add preliminary Switch 2 controller driver

 MAINTAINERS                          |   7 +
 drivers/hid/Kconfig                  |  18 +
 drivers/hid/Makefile                 |   1 +
 drivers/hid/hid-ids.h                |   4 +
 drivers/hid/hid-switch2.c            | 975 +++++++++++++++++++++++++++
 drivers/hid/hid-switch2.h            | 242 +++++++
 drivers/input/joystick/Kconfig       |   8 +
 drivers/input/joystick/Makefile      |   1 +
 drivers/input/joystick/switch2-usb.c | 353 ++++++++++
 9 files changed, 1609 insertions(+)
 create mode 100644 drivers/hid/hid-switch2.c
 create mode 100644 drivers/hid/hid-switch2.h
 create mode 100644 drivers/input/joystick/switch2-usb.c

-- 
2.51.2


^ permalink raw reply

* Re: (subset) [PATCH v6 0/7] mfd: macsmc: add rtc, hwmon and hid subdevices
From: Lee Jones @ 2026-01-08 17:33 UTC (permalink / raw)
  To: Sven Peter, Janne Grunau, Alyssa Rosenzweig, Neal Gompa,
	Lee Jones, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Alexandre Belloni, Jean Delvare, Guenter Roeck, Dmitry Torokhov,
	Jonathan Corbet, James Calligeros
  Cc: asahi, linux-arm-kernel, devicetree, linux-kernel, linux-rtc,
	linux-hwmon, linux-input, linux-doc, Hector Martin
In-Reply-To: <20251215-macsmc-subdevs-v6-0-0518cb5f28ae@gmail.com>

On Mon, 15 Dec 2025 19:37:44 +1000, James Calligeros wrote:
> This series adds support for the remaining SMC subdevices. These are the
> RTC, hwmon, and HID devices. They are being submitted together as the RTC
> and hwmon drivers both require changes to the SMC DT schema.
> 
> The RTC driver is responsible for getting and setting the system clock,
> and requires an NVMEM cell. This series replaces Sven's original RTC driver
> submission [1].
> 
> [...]

Applied, thanks!

[2/7] mfd: macsmc: Wire up Apple SMC RTC subdevice
      commit: a13cc4981449b9108921981a5e7c8824eaaa7604
[3/7] mfd: macsmc: Wire up Apple SMC hwmon subdevice
      commit: fb90c90aec3a9b7212a243f383f73a01bc653672
[5/7] mfd: macsmc: Wire up Apple SMC input subdevice
      commit: 3e9271dcb2dfdc4a98e9f6dc35c9be13276c3cfd

--
Lee Jones [李琼斯]


^ permalink raw reply

* [PATCH v4] hid/hid-multitouch: Keep latency normal on deactivate for reactivation gesture
From: Werner Sembach @ 2026-01-08 16:05 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires; +Cc: Werner Sembach, linux-input, linux-kernel

Uniwill devices have a built in gesture in the touchpad to de- and
reactivate it by double taping the upper left corner. This gesture stops
working when latency is set to high, so this patch keeps the latency on
normal.

Cc: stable@vger.kernel.org
Signed-off-by: Werner Sembach <wse@tuxedocomputers.com>
---
V1->V2: Use a quirk to narrow down the devices this is applied to.
V2->V3: Fix this patch breaking touchpads on some devices.
        Add another device ID.
V3->V4: Readd quirks formerly applied to the devices via the default class.

 drivers/hid/hid-multitouch.c | 32 +++++++++++++++++++++++++++++---
 1 file changed, 29 insertions(+), 3 deletions(-)

diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index 179dc316b4b51..9e4d89e56a351 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -76,6 +76,7 @@ MODULE_LICENSE("GPL");
 #define MT_QUIRK_DISABLE_WAKEUP		BIT(21)
 #define MT_QUIRK_ORIENTATION_INVERT	BIT(22)
 #define MT_QUIRK_APPLE_TOUCHBAR		BIT(23)
+#define MT_QUIRK_KEEP_LATENCY_ON_CLOSE	BIT(24)
 
 #define MT_INPUTMODE_TOUCHSCREEN	0x02
 #define MT_INPUTMODE_TOUCHPAD		0x03
@@ -211,6 +212,7 @@ static void mt_post_parse(struct mt_device *td, struct mt_application *app);
 #define MT_CLS_WIN_8_DISABLE_WAKEUP		0x0016
 #define MT_CLS_WIN_8_NO_STICKY_FINGERS		0x0017
 #define MT_CLS_WIN_8_FORCE_MULTI_INPUT_NSMU	0x0018
+#define MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE	0x0019
 
 /* vendor specific classes */
 #define MT_CLS_3M				0x0101
@@ -330,6 +332,15 @@ static const struct mt_class mt_classes[] = {
 			MT_QUIRK_CONTACT_CNT_ACCURATE |
 			MT_QUIRK_WIN8_PTP_BUTTONS,
 		.export_all_inputs = true },
+	{ .name = MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE,
+		.quirks = MT_QUIRK_ALWAYS_VALID |
+			MT_QUIRK_IGNORE_DUPLICATES |
+			MT_QUIRK_HOVERING |
+			MT_QUIRK_CONTACT_CNT_ACCURATE |
+			MT_QUIRK_STICKY_FINGERS |
+			MT_QUIRK_WIN8_PTP_BUTTONS |
+			MT_QUIRK_KEEP_LATENCY_ON_CLOSE,
+		.export_all_inputs = true },
 
 	/*
 	 * vendor specific classes
@@ -830,7 +841,8 @@ static int mt_touch_input_mapping(struct hid_device *hdev, struct hid_input *hi,
 			if ((cls->name == MT_CLS_WIN_8 ||
 			     cls->name == MT_CLS_WIN_8_FORCE_MULTI_INPUT ||
 			     cls->name == MT_CLS_WIN_8_FORCE_MULTI_INPUT_NSMU ||
-			     cls->name == MT_CLS_WIN_8_DISABLE_WAKEUP) &&
+			     cls->name == MT_CLS_WIN_8_DISABLE_WAKEUP ||
+			     cls->name == MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE) &&
 				(field->application == HID_DG_TOUCHPAD ||
 				 field->application == HID_DG_TOUCHSCREEN))
 				app->quirks |= MT_QUIRK_CONFIDENCE;
@@ -1709,7 +1721,8 @@ static int mt_input_configured(struct hid_device *hdev, struct hid_input *hi)
 	int ret;
 
 	if (td->is_haptic_touchpad && (td->mtclass.name == MT_CLS_WIN_8 ||
-	    td->mtclass.name == MT_CLS_WIN_8_FORCE_MULTI_INPUT)) {
+		td->mtclass.name == MT_CLS_WIN_8_FORCE_MULTI_INPUT ||
+		td->mtclass.name == MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE)) {
 		if (hid_haptic_input_configured(hdev, td->haptic, hi) == 0)
 			td->is_haptic_touchpad = false;
 	} else {
@@ -1998,7 +2011,12 @@ static void mt_on_hid_hw_open(struct hid_device *hdev)
 
 static void mt_on_hid_hw_close(struct hid_device *hdev)
 {
-	mt_set_modes(hdev, HID_LATENCY_HIGH, TOUCHPAD_REPORT_NONE);
+	struct mt_device *td = hid_get_drvdata(hdev);
+
+	if (td->mtclass.quirks & MT_QUIRK_KEEP_LATENCY_ON_CLOSE)
+		mt_set_modes(hdev, HID_LATENCY_NORMAL, TOUCHPAD_REPORT_NONE);
+	else
+		mt_set_modes(hdev, HID_LATENCY_HIGH, TOUCHPAD_REPORT_NONE);
 }
 
 /*
@@ -2375,6 +2393,14 @@ static const struct hid_device_id mt_devices[] = {
 		MT_USB_DEVICE(USB_VENDOR_ID_UNITEC,
 			USB_DEVICE_ID_UNITEC_USB_TOUCH_0A19) },
 
+	/* Uniwill touchpads */
+	{ .driver_data = MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE,
+		HID_DEVICE(BUS_I2C, HID_GROUP_MULTITOUCH_WIN_8,
+			USB_VENDOR_ID_PIXART, 0x0255) },
+	{ .driver_data = MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE,
+		HID_DEVICE(BUS_I2C, HID_GROUP_MULTITOUCH_WIN_8,
+			USB_VENDOR_ID_PIXART, 0x0274) },
+
 	/* VTL panels */
 	{ .driver_data = MT_CLS_VTL,
 		MT_USB_DEVICE(USB_VENDOR_ID_VTL,
-- 
2.43.0


^ permalink raw reply related

* [PATCH v5] hid/hid-multitouch: Keep latency normal on deactivate for reactivation gesture
From: Werner Sembach @ 2026-01-08 16:09 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires; +Cc: Werner Sembach, linux-input, linux-kernel

Uniwill devices have a built in gesture in the touchpad to de- and
reactivate it by double taping the upper left corner. This gesture stops
working when latency is set to high, so this patch keeps the latency on
normal.

Cc: stable@vger.kernel.org
Signed-off-by: Werner Sembach <wse@tuxedocomputers.com>
---
V1->V2: Use a quirk to narrow down the devices this is applied to.
V2->V3: Fix this patch breaking touchpads on some devices.
        Add another device ID.
V3->V4: Readd quirks formerly applied to the devices via the default class.
V4->V5: Fix whitespace error.

 drivers/hid/hid-multitouch.c | 32 +++++++++++++++++++++++++++++---
 1 file changed, 29 insertions(+), 3 deletions(-)

diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index 179dc316b4b51..62e7036806549 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -76,6 +76,7 @@ MODULE_LICENSE("GPL");
 #define MT_QUIRK_DISABLE_WAKEUP		BIT(21)
 #define MT_QUIRK_ORIENTATION_INVERT	BIT(22)
 #define MT_QUIRK_APPLE_TOUCHBAR		BIT(23)
+#define MT_QUIRK_KEEP_LATENCY_ON_CLOSE	BIT(24)
 
 #define MT_INPUTMODE_TOUCHSCREEN	0x02
 #define MT_INPUTMODE_TOUCHPAD		0x03
@@ -211,6 +212,7 @@ static void mt_post_parse(struct mt_device *td, struct mt_application *app);
 #define MT_CLS_WIN_8_DISABLE_WAKEUP		0x0016
 #define MT_CLS_WIN_8_NO_STICKY_FINGERS		0x0017
 #define MT_CLS_WIN_8_FORCE_MULTI_INPUT_NSMU	0x0018
+#define MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE	0x0019
 
 /* vendor specific classes */
 #define MT_CLS_3M				0x0101
@@ -330,6 +332,15 @@ static const struct mt_class mt_classes[] = {
 			MT_QUIRK_CONTACT_CNT_ACCURATE |
 			MT_QUIRK_WIN8_PTP_BUTTONS,
 		.export_all_inputs = true },
+	{ .name = MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE,
+		.quirks = MT_QUIRK_ALWAYS_VALID |
+			MT_QUIRK_IGNORE_DUPLICATES |
+			MT_QUIRK_HOVERING |
+			MT_QUIRK_CONTACT_CNT_ACCURATE |
+			MT_QUIRK_STICKY_FINGERS |
+			MT_QUIRK_WIN8_PTP_BUTTONS |
+			MT_QUIRK_KEEP_LATENCY_ON_CLOSE,
+		.export_all_inputs = true },
 
 	/*
 	 * vendor specific classes
@@ -830,7 +841,8 @@ static int mt_touch_input_mapping(struct hid_device *hdev, struct hid_input *hi,
 			if ((cls->name == MT_CLS_WIN_8 ||
 			     cls->name == MT_CLS_WIN_8_FORCE_MULTI_INPUT ||
 			     cls->name == MT_CLS_WIN_8_FORCE_MULTI_INPUT_NSMU ||
-			     cls->name == MT_CLS_WIN_8_DISABLE_WAKEUP) &&
+			     cls->name == MT_CLS_WIN_8_DISABLE_WAKEUP ||
+			     cls->name == MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE) &&
 				(field->application == HID_DG_TOUCHPAD ||
 				 field->application == HID_DG_TOUCHSCREEN))
 				app->quirks |= MT_QUIRK_CONFIDENCE;
@@ -1709,7 +1721,8 @@ static int mt_input_configured(struct hid_device *hdev, struct hid_input *hi)
 	int ret;
 
 	if (td->is_haptic_touchpad && (td->mtclass.name == MT_CLS_WIN_8 ||
-	    td->mtclass.name == MT_CLS_WIN_8_FORCE_MULTI_INPUT)) {
+	    td->mtclass.name == MT_CLS_WIN_8_FORCE_MULTI_INPUT ||
+	    td->mtclass.name == MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE)) {
 		if (hid_haptic_input_configured(hdev, td->haptic, hi) == 0)
 			td->is_haptic_touchpad = false;
 	} else {
@@ -1998,7 +2011,12 @@ static void mt_on_hid_hw_open(struct hid_device *hdev)
 
 static void mt_on_hid_hw_close(struct hid_device *hdev)
 {
-	mt_set_modes(hdev, HID_LATENCY_HIGH, TOUCHPAD_REPORT_NONE);
+	struct mt_device *td = hid_get_drvdata(hdev);
+
+	if (td->mtclass.quirks & MT_QUIRK_KEEP_LATENCY_ON_CLOSE)
+		mt_set_modes(hdev, HID_LATENCY_NORMAL, TOUCHPAD_REPORT_NONE);
+	else
+		mt_set_modes(hdev, HID_LATENCY_HIGH, TOUCHPAD_REPORT_NONE);
 }
 
 /*
@@ -2375,6 +2393,14 @@ static const struct hid_device_id mt_devices[] = {
 		MT_USB_DEVICE(USB_VENDOR_ID_UNITEC,
 			USB_DEVICE_ID_UNITEC_USB_TOUCH_0A19) },
 
+	/* Uniwill touchpads */
+	{ .driver_data = MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE,
+		HID_DEVICE(BUS_I2C, HID_GROUP_MULTITOUCH_WIN_8,
+			USB_VENDOR_ID_PIXART, 0x0255) },
+	{ .driver_data = MT_CLS_WIN_8_KEEP_LATENCY_ON_CLOSE,
+		HID_DEVICE(BUS_I2C, HID_GROUP_MULTITOUCH_WIN_8,
+			USB_VENDOR_ID_PIXART, 0x0274) },
+
 	/* VTL panels */
 	{ .driver_data = MT_CLS_VTL,
 		MT_USB_DEVICE(USB_VENDOR_ID_VTL,
-- 
2.43.0


^ permalink raw reply related

* hid-asus: integration of some rust code
From: Denis Benato @ 2026-01-08 13:36 UTC (permalink / raw)
  To: LKML; +Cc: Jiri Kosina, linux-input, Denis Benato, rust-for-linux

Hi all,

These past months I have been considering moving some userspace code to the kernel.

The userspace code is past of asusd and manages a feature of ASUS laptop called anime matrix:
a series of leds forming a display on the back.

The code in question was written by the previous maintainer of that application when
the kernel was not accepting any rust code, and now that it does I think it's about time
to port this well-tested feature where it belongs since it's talking directly to the hardware.


For context hid-asus must remain loaded and attached to the device as this new code
needs to manage just a subset of what is supported by the device, using the same hid_device*.

My (main) questions are the following:
- is this something that is acceptable?
- is this possible, and if yes what is the best way?
- is a mixed C/rust driver something that is accepted or does it break some rule?
- technically is this something that today the kernel has infrastructure for?

Specifically to Jiri: since you are the maintainer of hid-asus do you have any
objections in doing this?

Thanks to everyone who will respond and help me out.

Please feel free to add more considerations and answers questions I haven't
made since I'm sure there will be more anyway once I start working to this.

Best regards,
Denis B.


^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox