* [PATCH v7 2/3] iommu/arm-smmu-v3: Introduce CFGI/TLBI-repeat workaround infrastructure
From: Ashish Mhetre @ 2026-07-14 10:42 UTC (permalink / raw)
To: catalin.marinas, will, corbet, skhan, robin.murphy, joro,
nicolinc, jgg
Cc: linux-arm-kernel, linux-doc, linux-kernel, iommu, linux-tegra,
Ashish Mhetre
In-Reply-To: <20260714104202.1664187-1-amhetre@nvidia.com>
Tegra264 SMMU instances need every CFGI/TLBI command sequence issued
twice, with the second issue executing only after the first issue's
CMD_SYNC has completed:
TLBI/CFGI ... CMD_SYNC TLBI/CFGI ... CMD_SYNC
ATC_INV is not affected and must never be doubled.
Add arm_smmu_erratum_repeat_tlbi_cfgi_key and an
arm_smmu_erratum_cmd_needs_repeating() helper that gates on the static
key first and then range-checks the opcode (CFGI_STE .. ATC_INV), so
subsequent changes wiring the workaround into the CMDQ submission and
iommufd batching paths can share a single predicate.
Rename the existing arm_smmu_cmdq_issue_cmdlist() to
__arm_smmu_cmdq_issue_cmdlist() and add a thin wrapper that re-issues
the same cmdlist a second time when the predicate fires. Register the
new condition with arm_smmu_cmdq_batch_force_sync() and add
arm_vsmmu_can_batch_cmd() so iommufd batches split at every "needs
repeating" transition.
No callers enable the static key yet, so there is no functional change.
A subsequent change will enable the key on affected instances.
Suggested-by: Nicolin Chen <nicolinc@nvidia.com>
Reviewed-by: Nicolin Chen <nicolinc@nvidia.com>
Signed-off-by: Ashish Mhetre <amhetre@nvidia.com>
---
.../arm/arm-smmu-v3/arm-smmu-v3-iommufd.c | 14 ++++-
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c | 57 +++++++++++++++++--
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h | 1 +
3 files changed, 67 insertions(+), 5 deletions(-)
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c
index 1e9f7d2de344..143f0d015aeb 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c
@@ -350,6 +350,17 @@ static int arm_vsmmu_convert_user_cmd(struct arm_vsmmu *vsmmu,
return 0;
}
+static bool arm_vsmmu_can_batch_cmd(struct arm_vsmmu_invalidation_cmd *last,
+ struct arm_vsmmu_invalidation_cmd *next)
+{
+ struct arm_smmu_cmd next_cmd = {
+ .data[0] = le64_to_cpu(next->ucmd.cmd[0]),
+ };
+
+ return arm_smmu_erratum_cmd_needs_repeating(&last->cmd) ==
+ arm_smmu_erratum_cmd_needs_repeating(&next_cmd);
+}
+
int arm_vsmmu_cache_invalidate(struct iommufd_viommu *viommu,
struct iommu_user_data_array *array)
{
@@ -382,7 +393,8 @@ int arm_vsmmu_cache_invalidate(struct iommufd_viommu *viommu,
/* FIXME work in blocks of CMDQ_BATCH_ENTRIES and copy each block? */
cur++;
- if (cur != end && (cur - last) != CMDQ_BATCH_ENTRIES - 1)
+ if (cur != end && (cur - last) != CMDQ_BATCH_ENTRIES - 1 &&
+ arm_vsmmu_can_batch_cmd(last, cur))
continue;
/* FIXME always uses the main cmdq rather than trying to group by type */
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
index dd7475c50afc..9c49e6412053 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
@@ -18,6 +18,7 @@
#include <linux/interrupt.h>
#include <linux/io-pgtable.h>
#include <linux/iopoll.h>
+#include <linux/jump_label.h>
#include <linux/module.h>
#include <linux/msi.h>
#include <linux/of.h>
@@ -42,6 +43,14 @@ MODULE_PARM_DESC(disable_msipolling,
static const struct iommu_ops arm_smmu_ops;
static struct iommu_dirty_ops arm_smmu_dirty_ops;
+/*
+ * Repeat every {CFGI,TLBI};CMD_SYNC command sequence so that the second
+ * issue executes only after the first issue's CMD_SYNC has completed.
+ * Does not apply to ATC_INV. The key is global and is enabled from DT
+ * probe on affected hardware (currently Tegra264 only).
+ */
+static DEFINE_STATIC_KEY_FALSE(arm_smmu_erratum_repeat_tlbi_cfgi_key);
+
enum arm_smmu_msi_index {
EVTQ_MSI_INDEX,
GERROR_MSI_INDEX,
@@ -698,10 +707,10 @@ static void arm_smmu_cmdq_write_entries(struct arm_smmu_cmdq *cmdq,
* insert their own list of commands then all of the commands from one
* CPU will appear before any of the commands from the other CPU.
*/
-int arm_smmu_cmdq_issue_cmdlist(struct arm_smmu_device *smmu,
- struct arm_smmu_cmdq *cmdq,
- struct arm_smmu_cmd *cmds, int n,
- bool sync)
+static int __arm_smmu_cmdq_issue_cmdlist(struct arm_smmu_device *smmu,
+ struct arm_smmu_cmdq *cmdq,
+ struct arm_smmu_cmd *cmds, int n,
+ bool sync)
{
struct arm_smmu_cmd cmd_sync;
u32 prod;
@@ -820,6 +829,38 @@ int arm_smmu_cmdq_issue_cmdlist(struct arm_smmu_device *smmu,
return ret;
}
+bool arm_smmu_erratum_cmd_needs_repeating(struct arm_smmu_cmd *cmd)
+{
+ u8 opcode;
+
+ if (!static_branch_unlikely(&arm_smmu_erratum_repeat_tlbi_cfgi_key))
+ return false;
+
+ opcode = FIELD_GET(CMDQ_0_OP, cmd->data[0]);
+ return opcode >= CMDQ_OP_CFGI_STE && opcode < CMDQ_OP_ATC_INV;
+}
+
+int arm_smmu_cmdq_issue_cmdlist(struct arm_smmu_device *smmu,
+ struct arm_smmu_cmdq *cmdq,
+ struct arm_smmu_cmd *cmds, int n,
+ bool sync)
+{
+ int ret = __arm_smmu_cmdq_issue_cmdlist(smmu, cmdq, cmds, n, sync);
+
+ /*
+ * A bare CMD_SYNC can be issued with n == 0 (e.g. an empty
+ * batch_submit()), in which case there is no cmds[0] to inspect
+ * and nothing to repeat.
+ */
+ if (!n || ret || !sync)
+ return ret;
+
+ if (arm_smmu_erratum_cmd_needs_repeating(&cmds[0]))
+ ret = __arm_smmu_cmdq_issue_cmdlist(smmu, cmdq, cmds, n, sync);
+
+ return ret;
+}
+
static int arm_smmu_cmdq_issue_cmd_p(struct arm_smmu_device *smmu,
struct arm_smmu_cmd *cmd, bool sync)
{
@@ -860,6 +901,14 @@ static bool arm_smmu_cmdq_batch_force_sync(struct arm_smmu_device *smmu,
(smmu->options & ARM_SMMU_OPT_CMDQ_FORCE_SYNC))
return true;
+ /*
+ * See the description at arm_smmu_erratum_repeat_tlbi_cfgi_key. Batches
+ * never mix CFGI/TLBI with others, so checking cmds[0] alone is enough.
+ */
+ if (cmds->num == CMDQ_BATCH_ENTRIES &&
+ arm_smmu_erratum_cmd_needs_repeating(&cmds->cmds[0]))
+ return true;
+
return false;
}
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
index c909c9a88538..e175dedf7c77 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
@@ -1211,6 +1211,7 @@ int arm_smmu_cmdq_issue_cmdlist(struct arm_smmu_device *smmu,
struct arm_smmu_cmdq *cmdq,
struct arm_smmu_cmd *cmds, int n,
bool sync);
+bool arm_smmu_erratum_cmd_needs_repeating(struct arm_smmu_cmd *cmd);
#ifdef CONFIG_ARM_SMMU_V3_SVA
bool arm_smmu_sva_supported(struct arm_smmu_device *smmu);
--
2.50.1
^ permalink raw reply related
* [PATCH v7 3/3] iommu/arm-smmu-v3: Enable CFGI/TLBI-repeat workaround on Tegra264
From: Ashish Mhetre @ 2026-07-14 10:42 UTC (permalink / raw)
To: catalin.marinas, will, corbet, skhan, robin.murphy, joro,
nicolinc, jgg
Cc: linux-arm-kernel, linux-doc, linux-kernel, iommu, linux-tegra,
Ashish Mhetre
In-Reply-To: <20260714104202.1664187-1-amhetre@nvidia.com>
Nvidia Tegra264 SMMU is affected by an erratum where a TLB entry can
survive an invalidation that races with concurrent traffic targeting
the same entry. The hardware-recommended software workaround is to
issue every CFGI/TLBI command (each followed by CMD_SYNC) twice, and
that infrastructure is already in place behind
arm_smmu_erratum_repeat_tlbi_cfgi_key.
Neither IDR nor IIDR flags this Tegra264-specific bug, so hardware
detection is not possible. Tegra264 is device-tree-only (no ACPI/IORT
support) and already has a dedicated "nvidia,tegra264-smmu" compatible,
so DT-probe is the only viable detection path.
Enable the workaround on instances matching the existing
"nvidia,tegra264-smmu" compatible by calling static_branch_enable() on
arm_smmu_erratum_repeat_tlbi_cfgi_key. Document the erratum in
Documentation/arch/arm64/silicon-errata.rst.
Note that since guest-level VCMDQs issue commands directly to the
hardware, a guest kernel enabling the CMDQV feature on Tegra264 must
apply this workaround as well.
Reviewed-by: Nicolin Chen <nicolinc@nvidia.com>
Signed-off-by: Ashish Mhetre <amhetre@nvidia.com>
---
Documentation/arch/arm64/silicon-errata.rst | 2 ++
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c | 4 +++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/Documentation/arch/arm64/silicon-errata.rst b/Documentation/arch/arm64/silicon-errata.rst
index 014aa1c215a1..076b3947d259 100644
--- a/Documentation/arch/arm64/silicon-errata.rst
+++ b/Documentation/arch/arm64/silicon-errata.rst
@@ -312,6 +312,8 @@ stable kernels.
| | | T241-MPAM-4, | |
| | | T241-MPAM-6 | |
+----------------+-----------------+-----------------+-----------------------------+
+| NVIDIA | T264 SMMU | T264-SMMU-3 | N/A |
++----------------+-----------------+-----------------+-----------------------------+
+----------------+-----------------+-----------------+-----------------------------+
| Freescale/NXP | LS2080A/LS1043A | A-008585 | FSL_ERRATUM_A008585 |
+----------------+-----------------+-----------------+-----------------------------+
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
index 9c49e6412053..a04dea34479c 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
@@ -5357,8 +5357,10 @@ static int arm_smmu_device_dt_probe(struct platform_device *pdev,
if (of_dma_is_coherent(dev->of_node))
smmu->features |= ARM_SMMU_FEAT_COHERENCY;
- if (of_device_is_compatible(dev->of_node, "nvidia,tegra264-smmu"))
+ if (of_device_is_compatible(dev->of_node, "nvidia,tegra264-smmu")) {
tegra_cmdqv_dt_probe(dev->of_node, smmu);
+ static_branch_enable(&arm_smmu_erratum_repeat_tlbi_cfgi_key);
+ }
return ret;
}
--
2.50.1
^ permalink raw reply related
* [PATCH v7 1/3] iommu/arm-smmu-v3: Factor out CMDQ batch force-sync conditions
From: Ashish Mhetre @ 2026-07-14 10:42 UTC (permalink / raw)
To: catalin.marinas, will, corbet, skhan, robin.murphy, joro,
nicolinc, jgg
Cc: linux-arm-kernel, linux-doc, linux-kernel, iommu, linux-tegra,
Ashish Mhetre
In-Reply-To: <20260714104202.1664187-1-amhetre@nvidia.com>
From: Nicolin Chen <nicolinc@nvidia.com>
arm_smmu_cmdq_batch_add_cmd_p() carries two distinct reasons for
flushing the current batch with a CMD_SYNC before appending the
new command:
- The batch's pre-assigned cmdq does not support the new command.
- The Arm erratum 2812531 workaround (ARM_SMMU_OPT_CMDQ_FORCE_SYNC)
forces a SYNC at one entry before the batch is full.
Lift those checks into a new arm_smmu_cmdq_batch_force_sync() helper
so that adding another force-sync condition becomes a one-line
addition. No functional change.
Signed-off-by: Nicolin Chen <nicolinc@nvidia.com>
Signed-off-by: Ashish Mhetre <amhetre@nvidia.com>
---
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c | 23 +++++++++++++++------
1 file changed, 17 insertions(+), 6 deletions(-)
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
index 57b750ebcd3d..dd7475c50afc 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
@@ -847,16 +847,27 @@ static void arm_smmu_cmdq_batch_init_cmd(struct arm_smmu_device *smmu,
cmds->cmdq = arm_smmu_get_cmdq(smmu, cmd);
}
+static bool arm_smmu_cmdq_batch_force_sync(struct arm_smmu_device *smmu,
+ struct arm_smmu_cmdq_batch *cmds,
+ struct arm_smmu_cmd *cmd)
+{
+ /* The batch's pre-assigned cmdq doesn't support the new command */
+ if (!arm_smmu_cmdq_supports_cmd(cmds->cmdq, cmd))
+ return true;
+
+ /* Arm erratum 2812531 */
+ if (cmds->num == CMDQ_BATCH_ENTRIES - 1 &&
+ (smmu->options & ARM_SMMU_OPT_CMDQ_FORCE_SYNC))
+ return true;
+
+ return false;
+}
+
static void arm_smmu_cmdq_batch_add_cmd_p(struct arm_smmu_device *smmu,
struct arm_smmu_cmdq_batch *cmds,
struct arm_smmu_cmd *cmd)
{
- bool force_sync = (cmds->num == CMDQ_BATCH_ENTRIES - 1) &&
- (smmu->options & ARM_SMMU_OPT_CMDQ_FORCE_SYNC);
- bool unsupported_cmd;
-
- unsupported_cmd = !arm_smmu_cmdq_supports_cmd(cmds->cmdq, cmd);
- if (force_sync || unsupported_cmd) {
+ if (arm_smmu_cmdq_batch_force_sync(smmu, cmds, cmd)) {
arm_smmu_cmdq_issue_cmdlist(smmu, cmds->cmdq, cmds->cmds,
cmds->num, true);
arm_smmu_cmdq_batch_init_cmd(smmu, cmds, cmd);
--
2.50.1
^ permalink raw reply related
* [PATCH v7 0/3] iommu/arm-smmu-v3: Tegra264 invalidation workaround
From: Ashish Mhetre @ 2026-07-14 10:41 UTC (permalink / raw)
To: catalin.marinas, will, corbet, skhan, robin.murphy, joro,
nicolinc, jgg
Cc: linux-arm-kernel, linux-doc, linux-kernel, iommu, linux-tegra,
Ashish Mhetre
Nvidia Tegra264 SMMUs are affected by an erratum where a TLB entry can
survive an invalidation that races with concurrent traffic targeting
the same entry. The hardware-recommended software workaround is to
issue every CFGI/TLBI command (each followed by CMD_SYNC) twice.
The second issue must execute only after the first issue's CMD_SYNC
has completed, giving the sequence:
TLBI/CFGI ... CMD_SYNC TLBI/CFGI ... CMD_SYNC
ATC_INV is not affected and must not be doubled.
The erratum is not flagged by any SMMUv3 IDR/IIDR register, so it
cannot be detected from hardware ID. Tegra264 is device-tree-only
(no ACPI/IORT support), so detection is purely by compatible string.
This series is structured as a small refactor + infrastructure + enable
sequence so that each step is reviewable in isolation:
1/3 Pure refactor (no functional change): lift the existing
force-sync conditions out of arm_smmu_cmdq_batch_add_cmd_p()
into a new arm_smmu_cmdq_batch_force_sync() helper, so that
adding another condition (in patch 2) is a one-line addition.
Authored by Nicolin Chen.
2/3 Add the workaround infrastructure without enabling it. Defines
the file-local arm_smmu_erratum_repeat_tlbi_cfgi_key static key
with an inline erratum description, the shared
arm_smmu_erratum_cmd_needs_repeating() predicate, the
arm_smmu_cmdq_issue_cmdlist() wrapper that can re-issue matching
cmdlists, the batch-helper force-sync condition, and the iommufd
batching split for mixed command classes.
3/3 Enable the workaround for the existing "nvidia,tegra264-smmu"
compatible and document the erratum in silicon-errata.rst.
The series applies cleanly on linux-next/master (base-commit below).
Changes since v6:
- Add #include <linux/jump_label.h> now that the static key is
defined in arm-smmu-v3.c.
- Drop the unused smmu parameter from arm_vsmmu_can_batch_cmd().
- Expand the arm_smmu_cmdq_batch_force_sync() comment to note that
batches never mix CFGI/TLBI with other commands, so checking
cmds[0] alone is enough.
- Note in 3/3 that a guest kernel enabling CMDQV on Tegra264 must
also apply this workaround, since guest-level VCMDQs issue
commands directly to the hardware.
- Carry Reviewed-by: Nicolin Chen on 2/3 and 3/3.
Changes since v5:
- Move arm_smmu_erratum_cmd_needs_repeating() into arm-smmu-v3.c
and leave a declaration-only stub in arm-smmu-v3.h. Make
arm_smmu_erratum_repeat_tlbi_cfgi_key file-local static.
- Add an inline erratum/workaround description at the static key,
referenced from arm_smmu_cmdq_batch_force_sync().
- Fix (rather than drop) the misleading !n comment above
arm_smmu_cmdq_issue_cmdlist(); keep the defensive !n guard.
- Remove the unused smmu parameter from the predicate.
- Tweak 2/3 commit-message wording ("commit" vs "patch").
Changes since v4:
- Drop ARM_SMMU_OPT_REPEAT_TLBI_CFGI entirely: the option bit was
set and read on the exact same "nvidia,tegra264-smmu" compatible
as the static key, so it added no per-instance signal that the
static key did not already carry. The predicate now gates purely
on arm_smmu_erratum_repeat_tlbi_cfgi_key.
- Reorder the series so the compatible-string detection lands
last, once all the infrastructure exists:
1/3 factor out force_sync helper (unchanged)
2/3 add static key + WAR functions (no functional change)
3/3 enable the key on nvidia,tegra264-smmu + silicon-errata
Split the old v4 "Detect" and "Issue twice" patches accordingly.
- Update the /* See ARM_SMMU_OPT_REPEAT_TLBI_CFGI */ comment inside
arm_smmu_cmdq_batch_force_sync() to reference the static key
description instead.
Changes since v3:
- Drop the cmds->num == 0 early-return so the refactor is
truly "no functional change".
- Rename ARM_SMMU_OPT_TLBI_TWICE -> ARM_SMMU_OPT_REPEAT_TLBI_CFGI
and rephrase its kdoc to be hardware-agnostic.
- Rename arm_smmu_cmd_needs_tlbi_twice() ->
arm_smmu_erratum_cmd_needs_repeating() and drop the kdoc
above it.
- Replace the explicit opcode switch with a single range check
opcode >= CMDQ_OP_CFGI_STE && opcode < CMDQ_OP_ATC_INV.
- Introduce arm_smmu_erratum_repeat_tlbi_cfgi_key static key:
the predicate gates on it first so unaffected kernels pay
only a single static_branch_unlikely() check.
- Drop the verbose Tegra264-specific comments above
arm_vsmmu_can_batch_cmd() and inside the batch helper.
- Document the erratum in
Documentation/arch/arm64/silicon-errata.rst.
- Guard the repeat path in arm_smmu_cmdq_issue_cmdlist() with
an n > 0 check so cmds[0] is never inspected on an empty
cmdlist.
- Drop the carried Reviewed-by tags now that the patch
shape has changed; re-review appreciated.
Changes since v2:
- Split into a 3-patch series (refactor / detect / apply) to keep
each step small and bisectable.
- Move the classifier to arm-smmu-v3.h as static inline so the
iommufd file can share it.
- Add arm_vsmmu_can_batch_cmd() to split iommufd batches at
"needs repeating" transitions so the per-batch decision based
on the first command stays correct under mixed user input.
- Spell out in the commit message why detection is via DT and
not via IIDR/ACPI.
Changes since v1:
- Detect the erratum from the existing "nvidia,tegra264-smmu"
compatible instead of adding a new property.
- Centralise the doubling at the CMDQ submission layer and only
apply it to CFGI/TLBI (not ATC_INV).
- Drop the binding/dtsi patches accordingly.
Ashish Mhetre (2):
iommu/arm-smmu-v3: Introduce CFGI/TLBI-repeat workaround
infrastructure
iommu/arm-smmu-v3: Enable CFGI/TLBI-repeat workaround on Tegra264
Nicolin Chen (1):
iommu/arm-smmu-v3: Factor out CMDQ batch force-sync conditions
Documentation/arch/arm64/silicon-errata.rst | 2 +
.../arm/arm-smmu-v3/arm-smmu-v3-iommufd.c | 14 +++-
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c | 84 ++++++++++++++++---
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h | 1 +
4 files changed, 89 insertions(+), 12 deletions(-)
base-commit: bee763d5f341b99cf472afeb508d4988f62a6ca1
--
2.50.1
^ permalink raw reply
* [PATCH] docs: pt_BR: process: Translate the security-bugs.rst
From: Álysson Gleyson da Silva @ 2026-07-14 10:26 UTC (permalink / raw)
To: danielmaraboo; +Cc: linux-doc, Álysson Gleyson da Silva
Translate the documentation on security bugs into Brazilian Portuguese,
maintaining consistency with the original formatting rules.
Signed-off-by: Álysson Gleyson da Silva <alyssongleyson.dev@gmail.com>
---
Documentation/translations/pt_BR/index.rst | 1 +
.../pt_BR/process/security-bugs.rst | 370 ++++++++++++++++++
2 files changed, 371 insertions(+)
create mode 100644 Documentation/translations/pt_BR/process/security-bugs.rst
diff --git a/Documentation/translations/pt_BR/index.rst b/Documentation/translations/pt_BR/index.rst
index 7a488f662..796fe9bca 100644
--- a/Documentation/translations/pt_BR/index.rst
+++ b/Documentation/translations/pt_BR/index.rst
@@ -78,3 +78,4 @@ kernel e sobre como ver seu trabalho integrado.
Processo do subsistema SoC <process/maintainer-soc>
Conformidade de DTS para SoC <process/maintainer-soc-clean-dts>
Processo do subsistema KVM x86 <process/maintainer-kvm-x86>
+ Falhas de segurança <process/security-bugs>
diff --git a/Documentation/translations/pt_BR/process/security-bugs.rst b/Documentation/translations/pt_BR/process/security-bugs.rst
new file mode 100644
index 000000000..a0c57219b
--- /dev/null
+++ b/Documentation/translations/pt_BR/process/security-bugs.rst
@@ -0,0 +1,370 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+.. _securitybugs_pt_BR:
+
+Falhas de segurança
+===================
+
+Os desenvolvedores do kernel Linux levam a segurança muito a sério. Como tal,
+gostariamos de saber quando uma falha de segurança é encontrada para que ela
+possa ser corrigida e divulgada o mais rápido possível.
+
+Preparando seu relatório
+------------------------
+
+Como em qualquer relatório de bug, um relátorio de falha de segurança exige
+muito trabalho de análise por parte dos desenvolvedores, portanto, quanto mais
+informações você puder compartilhar sobre o problema, melhor. Por favor, revise
+o procedimento descrito em Documentation/admin-guide/reporting-issues.rst se
+você não tiver certeza sobre quais informações são úteis. As seguintes
+informações são absolutamente necessárias em **qualquer** relatório de falha de
+segurança:
+
+ * **versão do kernel afetada**: sem indicação de versão, seu relatório não
+ será processado. Uma parte significativa dos relatórios é de bugs que já
+ foram corrigidos, portanto, é extremamente importante que as
+ vulnerabilidades sejam verificadas em versões recentes (árvore de
+ desenvolvimento ou a versão estável mais recente), pelo menos verificando
+ se o código não mudou desde a versão onde foi detectado.
+
+ * **descrição do problema**: uma descrição detalhada do problema, com rastros
+ mostrando sua manifestação, e por que você considera o comportamento
+ observado como um problema no Kernel, é necessaária.
+
+ * **reproduzir**: os desenvolvedores precisarão ser capazes de reproduzir o
+ problema para considerar uma correção como eficaz. Isso inclui tanto uma
+ maneira de acionar o problema quanto uma maneira de confirmar que ele
+ ocorre. Será necessário um reprodutor com dependências de baixa
+ complexidade (código-fonte, script de shell, seguência de instruções,
+ imagem de sistema de arquivos, etc). Executáveis apenas binários não são
+ aceitos. Exploits funcionais são extremamente úteis e não serão divulgados
+ sem o consentimento do relator, a menos que já sejam públicos. Por
+ definição, se um problema não pode ser reproduzido, ele não é explorável,
+ portanto, não é um bug de segurança.
+
+ * **condições**: se o bug depender de certas opções de configuração, sysctls,
+ permissões, temporização, modificações de código, etc., estas devem ser
+ indicadas.
+
+Além disso, as seguintes informaçẽos são altamente desejáveis:
+
+ * **localização suspeita do bug**: os nomes dos arquivos e funções onde
+ se suspeita que o bug esteja presente são muito importantes, pelo menos
+ para ajudar a encaminhar o relatório aos mantenedores apropriados. Quando
+ não for possível (por exemplo, "o sistema trava toda vez que executo este
+ comando"), a equipe de segurança ajudará a identificar a origem do bug.
+
+ * **uma proposta de correção**: os relatores de bugs que analisaram a causa
+ de uma falha no código-fonte quase sempre têm uma ideia precisa de como
+ corrigi-lo, porque passaram muito tempo estudando o problema e suas
+ implicações. Propor uma correção testada poupará muito tempo dos
+ mantenedores, mesmo que a correção acabe não sendo a correta, pois ajuda a
+ entender o bug. A propor uma correção testada, por favor, formate-a
+ sempre de uma maneira que possa ser mesclada imediatamente (consulte
+ Documentation/process/submitting-patches.rst). Isso evitará algumas trocas
+ de mensagens caso ela seja aceita, e você receberá o crédito por
+ encontrar e corrigir o problema. Observe que, neste caso, apenas uma tag
+ ``Signed-off-by:`` é necessária, sem ``Reported-by:`` quando o relator e
+ o autor forem a mesma pessoa.
+
+ * **mitigações**: com muita frequência, durante a análise de um bug,
+ surgem algumas maneiras de mitigar o problema. È útil compartilhá-las,
+ pois podem ser úteis para manter os usuários finais protegidos durante o
+ tempo que levam para aplicar a correção.
+
+O que se qualifica como um bug de segurança
+-------------------------------------------
+
+É importante que a maioria dos bugs seja tratada publicamente, de modo a
+envolver o maior público possível e encontrar a melhor solução. Por natureza,
+bugs que são tratados em discussões fechadas entre um pequeno conjunto de
+participantes têm menos probabilidade de produzir a melhor correção possível
+(por exemplo, risco de perder casos de uso válidos, capacidades de testes
+limitadas).
+
+Acontece que a maioria dos bugs relatados por meio da equipe de segurança são
+apenas bugs comuns que foram qualificados incorretamente como bugs de segurança
+devido à falta de conhecimento do modelo de ameaças do kernel Linux, conforme
+descrito em Documentation/process/threat-model.rst, e deveriam ter sido
+enviados através dos canais normais descritos em
+Documentation/admin-guide/reporting-issues.rst
+
+A lista de segurança existe para bugs urgentes que concedem a um atacante uma
+capacidade que ele não deveria ter em um sistema de produção corretamente
+configurado, e que podem ser facilmente explorados, representando uma ameaça
+iminente para muitos usuários. Antes de relatar, considere se o problema
+realmente ultrapassa um limite de confiança em tal sistema.
+
+**Se você recorreu a assistência de IA para identificar um bug, você deve
+tratá-lo como público**. Embora você possa ter motivos válidos para acreditar
+que não seja, a experiência da equipe de segurança mostra que os bugs
+descobertos desta forma surgem sistematicamente e de forma simultânea entre
+múltiplos pesquisadores, frequentemente no mesmo dia. Neste caso, não
+compartilhe publicamente um reprodutor, pois isso poderia causar danos não
+intencionais; apenas mencione que um está disponível e os mantenedores poderão
+solicitá-lo privadamente se precisarem.
+
+Se você não tiver certeza se um problema se qualifica, opte por relatar de
+forma privada: a equipe de segurança prefere triar um relatório limítrofe
+a perder uma vulnerabilidade real. Relatar bugs comuns na lista de segurança,
+no entanto, não faz com que eles andem mais rápido e consome a capacidade de
+triagem de que outros relatórios precisam.
+
+Identificando contatos
+----------------------
+
+A maneira mais eficaz de relatar um bug de segurança é enviá-lo diretamente
+aos mantenedores do subsistema afetado e Cc: para a equipe de segurança do
+kernel Linux. Não o envie para uma lista pública nesta fase, a menos que você
+tenha bons motivos para considerar o problema como público ou trivial de ser
+descoberto (por exemplo, resultado de uma ferramenta automatizada de varredura
+de vulnerabilidades amplamente disponível que possa ser repetida por qualquer
+pessoa, ou o uso de ferramentas baseadas em IA).
+
+Se você estiver enviando um relatório de problemas que afetam várias partes no
+kernel, mesmo que sejam problemas bastante semelhantes, envie mensagens
+individuais (pense que os mantenedores não trabalharão todos nos problemas ao
+mesmo tempo). A única exceção é quando um problema diz respeito a partes
+intimamente relacionadas, mantidas pelo exato mesmo subconjunto de
+mantenedores, e espera-se que essas partes sejam todas corrigidas de uma só vez
+pelo mesmo commit; então pode ser aceitável relatá-las de uma vez.
+
+Uma dificuldade para a maioria dos relatores de primeira viagem é descobrir a
+lista certa de destinatários para enviar um relatório. No kernel Linux, todos
+os mantenedores oficiais são confiáveis, portanto as consequências de incluir
+acidentalmente o mantenedor errado são apenas um pequeno ruido para essa
+pessoa, ou seja, nada dramático. Sendo assim, um método adequado para descobrir
+a lista de mantenedores (o qual os oficiais de segurança do kernel usam) é
+contar com o script get_maintainer.pl, ajustado para relatar apenas
+mantenedores. Este script, quando recebe um nome de arquivo, procurará por seu
+caminho no arquivo MAINTAINERS para deduzir uma lista hierárquica de
+mantenedores relevantes. Chamá-lo pela primeira vez com o nível mais refinado
+de filtragem retornará, na maioria das vezes, uma lista curta de mantenedores
+deste arquivo específico::
+
+ $ ./scripts/get_maintainer.pl --no-l --no-r --pattern-depth 1 \
+ drivers/example.c
+ Developer One <dev1@example.com> (maintainer:example driver)
+ Developer Two <dev2@example.org> (maintainer:example driver)
+
+Estes dois mantenedores devem então receber a mensagem. Se o comando não
+retornar nada, isso significa que o arquivo afetado faz parte de um subsistema
+mais amplo, portanto devemos ser menos específicos::
+
+ $ ./scripts/get_maintainer.pl --no-l --no-r drivers/example.c
+ Developer One <dev1@example.com> (maintainer:example subsystem)
+ Developer Two <dev2@example.org> (maintainer:example subsystem)
+ Developer Three <dev3@example.com> (maintainer:example subsystem [GENERAL])
+ Developer Four <dev4@example.org> (maintainer:example subsystem [GENERAL])
+
+Aqui, escolher os primeiros, mais específicos, é suficiente. Quando a lista for
+longa, é possível produzir uma lista de endereços de e-mail delimitada por
+vírgulas em uma única linha adequada para o uso no campo TO: de um cliente de
+e-mail como este::
+
+ $ ./scripts/get_maintainer.pl --no-tree --no-l --no-r --no-n --m \
+ --no-git-fallback --no-substatus --no-rolestats --no-multiline \
+ --pattern-depth 1 drivers/example.c
+ dev1@example.com, dev2@example.org
+
+ou este para a lista mais ampla::
+
+ $ ./scripts/get_maintainer.pl --no-tree --no-l --no-r --no-n --m \
+ --no-git-fallback --no-substatus --no-rolestats --no-multiline \
+ drivers/example.c
+ dev1@example.com, dev2@example.org, dev3@example.com, dev4@example.org
+
+Se a esta altura você ainda estiver enfrentando dificuldades para identificar
+os mantenedores corretos, e apenas neste caso, é possível enviar seu
+relatório apenas para a equipe de segurança do kernel Linux. Sua mensagem
+será triada e você receberá instruções sobre quem contatar, se necessário.
+Sua mensagem poderá igualmente ser encaminhada como está para os mantenedores
+relevantes.
+
+Uso responsável de IA para encontrar bugs
+-----------------------------------------
+
+Uma fração significativa dos relatórios de bugs enviados à equipe de segurança
+é, na verdade, o resultado de revisões de código assistidas por ferramentas de
+IA. Embora isso possa ser um meio eficiente de encontrar bugs em áreas
+raramente exploradas, causa uma sobrecarga nos mantenedores, que às vezes são
+forçados a ignorar tais relatórios devido à sua má qualidade ou precisão. Sendo
+assim, os relatores devem ter um cuidado especial com vários pontos que tendem
+a tornar esses relatórios desnecessariamente difíceis de lidar:
+
+ * **Comprimento**: Os relatórios gerados por IA tendem a ser excessivamente
+ longos, contendo várias seções e detalhes em excesso. Isso dificulta a
+ identificação de informações importantes, como arquivos afetados, versões e
+ impacto. Por favor, certifique-se de que um resumo claro do problema e
+ todos os detalhes críticos sejam apresentados primeiro. Não exija que os
+ engenheiros de triagem analisem várias páginas de texto. Configure suas
+ ferramentas para produzir relatórios concisos e em estilo humano.
+
+ * **Formatação**: A maioria dos relatórios gerados por IA está repleta de
+ tags Markdown. Essas decorações complicam a busca por informações
+ importantes e não sobrevivem aos processos de citação envolvidos no
+ encaminhamento ou nas respostas. Por favor, sempre converta seu relatório
+ para texto simples sem quaisquer decorações de formatação antes de
+ enviá-lo.
+
+ * **Avaliação de Impacto**: Muitos relatórios gerados por IA carecem de uma
+ compreensão do modelo de ameaças do kernel (consulte
+ Documentation/process/threat-model.rst) e fazem de tudo para inventar
+ consequências teóricas. Isso adiciona ruído e complica a triagem. Por
+ favor, limite-se a fatos verificáveis (por exemplo, "este bug permite que
+ qualquer usuário obtenha CAP_NET_ADMIN") sem enumerar implicações
+ especulativas. Faça com que sua ferramenta leia esta documentação como
+ parte do processo de avaliação.
+
+ * **Reproduzidor**: As ferramentas baseadas em IA são frequentemente capazes
+ de gerar reproduzidores. Por favor, certifique-se sempre de que sua
+ ferramenta forneça um e teste-o exaustivamente. Se o reproduzidor não
+ funcionar, ou se a ferramenta não puder produzir um, a validade do
+ relatório deve ser seriamente questionada. Observe que, como o relatório
+ será postado em uma lista pública, o reproduzidor só deve ser compartilhado
+ mediante solicitação dos mantenedores.
+
+ * **Propor uma Correção:** muitas ferramentas de IA são na verdade melhores
+ em escrever código do que em avaliá-lo. Por favor, peça à sua ferramenta
+ para propor uma correção e teste-a antes de relatar o problema.
+ Se a correção não puder ser testada porque depende de hardware raro ou de
+ protocolos de rede quase extintos, é provável que o problema não seja um
+ bug de segurança. Em qualquer caso, se uma correção for proposta, ela deve
+ aderir a Documentation/process/submitting-patches.rst e incluir uma tag
+ 'Fixes:' designando o commit que introduziu o bug.
+
+A falha em considerar estes pontos expõe seu relatório ao risco de ser
+ignorado.
+
+Use o bom senso ao avaliar o relatório. Se o arquivo afetado não tiver sido
+alterado por mais de um ano e for mantido por um único indivíduo, é provável
+que o uso tenha diminuído e os usuários expostos sejam virtualmente
+inexistentes (por exemplo, drivers para hardware muito antigo, sistemas de
+arquivos obsoletos). Nesses casos, não há necessidade de consumir o tempo de
+um mantenedor com um relatório sem importância. Se o problema for claramente
+trivial e publicamente detectável, você deve relatá-lo diretamente às listas
+de discussão públicas.
+
+Enviando o relatório
+--------------------
+
+Os relatórios devem ser enviados exclusivamente por e-mail. Por favor, use um
+endereço de e-mail funcional, de preferência o mesmo que você deseja que
+apareça nas tags ``Reported-by``, se houver. Se não tiver certeza, envie o seu
+relatório para você mesmo primeiro.
+
+A equipe de segurança e os mantenedores quase sempre exigem informações
+adicionais além das fornecidas inicialmente em um relatório e dependem de uma
+colaboração ativa e eficiente com o relator para realizar testes adicionais
+(por exemplo, verificar versões, opções de configuração, mitigações ou
+patches). Antes de entrar em contato com a equipe de segurança, o relator deve
+certificar-se de que está disponível para explicar suas descobertas, participar
+de discussões e executar testes adicionais. Relatórios nos quais o relator não
+responde prontamente ou não consegue discutir suas descobertas de forma eficaz
+podem ser abandonados se a comunicação não melhorar rapidamente.
+
+O relatório deve ser enviado aos mantenedores. Se houver dois ou menos
+destinatários em sua mensagem, você também deve sempre colocar em Cc: a equipe
+de segurança do kernel Linux, que garantirá que a mensagem seja entregue às
+pessoas corretas e poderá auxiliar pequenas equipes de mantenedores com
+processos com os quais eles possam não estar familiarizados. Para equipes
+maiores, coloque em Cc: a equipe de segurança do kernel Linux em seus primeiros
+relatórios ou ao buscar ajuda específica, como ao reenviar uma mensagem que não
+obteve resposta dentro de uma semana. Assim que você se sentir confortável com
+o processo após alguns relatórios, não será mais necessário colocar a lista de
+segurança em Cc: ao enviar para equipes grandes. A equipe de segurança do
+kernel Linux pode ser contatada por e-mail em security@kernel.org. Esta é uma
+lista privada de oficiais de segurança que ajudarão a verificar o relatório de
+bug e auxiliarão os desenvolvedores que trabalham em uma correção. É possível
+que a equipe de segurança traga ajuda extra de mantenedores da área para
+entender e corrigir a vulnerabilidade de segurança.
+
+Por favor, envie e-mails em **texto simples** sem anexos, sempre que possível.
+É muito mais difícil ter uma discussão com citações de contexto sobre um
+problema complexo se todos os detalhes estiverem ocultos em anexos. Pense nisso
+como uma :doc:`regular path submission <../process/submitting-patches>`
+(mesmo que você ainda não tenha um patch): descreva o problema e o impacto,
+liste as etapas de reprodução e siga com uma proposta de correção, tudo em
+texto simples. Relatórios formatados em Markdown, HTML e RST são
+particularmente malvistos, pois são bastante difíceis de ler por humanos e
+incentivam o uso de visualizadores dedicados, às vezes online, o que por
+definição não é aceitável para um relatório de segurança confidencial. Note
+que alguns clientes de e-mail tendem a corromper a formatação de texto simples
+por padrão; por favor, consulte Documentation/process/email-clients.rst para
+mais informações.
+
+Divulgação e informações sob embargo
+------------------------------------
+
+A lista de segurança não é um canal de divulgação. Para isso, veja Coordenação
+abaixo.
+
+Assim que uma correção robusta for desenvolvida, o processo de lançamento é
+iniciado. Correções para bugs publicamente conhecidos são lançadas
+imediatamente.
+
+Embora nossa preferência seja lançar correções para bugs publicamente não
+divulgados assim que estiverem disponíveis, isso pode ser adiado a pedido do
+relator ou de uma parte afetada por até 7 dias corridos a partir do início do
+processo de lançamento, com uma extensão excepcional para 14 dias corridos se
+for acordado que a criticidade do bug exige mais tempo. O único motivo válido
+para adiar a publicação de uma correção é acomodar a logística de QA e as
+implantações em larga escala que exigem coordenação de lançamento.
+
+Embora as informações sob embargo possam ser compartilhadas com indivíduos de
+confiança para o desenvolvimento de uma correção, tais informações não serão
+publicadas juntamente com a correção ou em qualquer outro canal de divulgação
+sem a permissão do relator. Isso inclui, mas não se limita ao relatório de bug
+original e discussões de acompanhamento (se houver), exploits, informações de
+CVE ou a identidade do relator.
+
+Em outras palavras, nosso único interesse é fazer com que os bugs sejam
+corrigidos. Todas as outras informações enviadas à lista de segurança e
+quaisquer discussões de acompanhamento do relatório são tratadas de forma
+confidencial, mesmo após o término do embargo, perpetuamente.
+
+Coordenação com outros grupos
+-----------------------------
+
+Embora a equipe de segurança do kernel se concentre exclusivamente em corrigir
+bugs, outros grupos se concentram em corrigir problemas em distribuições e em
+coordenar a divulgação entre fornecedores de sistemas operacionais. A
+coordenação é geralmente tratada pela lista de discussão "linux-distros" e a
+divulgação pela lista pública "oss-security", ambas intimamente relacionadas
+e apresentadas na wiki da linux-distros:
+https://oss-security.openwall.org/wiki/mailing-lists/distros
+
+Por favor, note que as respectivas políticas e regras são diferentes, já que as
+3 listas buscam objetivos distintos. A coordenação entre a equipe de segurança
+do kernel e outras equipes é difícil porque para a equipe de segurança do
+kernel os embargos ocasionais (sujeitos a um número máximo de dias permitido)
+começam a partir da disponibilidade de uma correção, enquanto para a
+"linux-distros" eles começam a partir da postagem inicial na lista,
+independentemente da disponibilidade de uma correção.
+
+Como tal, a equipe de segurança do kernel recomenda fortemente que, como
+relator de um potencial problema de segurança, você NÃO contate a lista de
+discussão "linux-distros" ATÉ que uma correção seja aceita pelos mantenedores
+do código afetado e você tenha lido a página wiki das distribuições acima e
+compreendido totalmente os requisitos que o contato com a "linux-distros"
+imporá a você e à comunidade do kernel. Isso também significa que, em geral,
+não faz sentido colocar ambas as listas em Cc: ao mesmo tempo, exceto talvez
+para coordenação se e enquanto uma correção aceita ainda não tiver sido
+mesclada. Em outras palavras, até que uma correção seja aceita, não coloque
+em Cc: "linux-distros", e após ela ser mesclada, não coloque em Cc: a equipe
+de segurança do kernel.
+
+Atribuição de CVE
+-----------------
+
+A equipe de segurança não atribui CVEs, nem os exigimos para relatórios ou
+correções, pois isso pode complicar desnecessariamente o processo e adiar o
+tratamento do bug. Se um relator desejar que um identificador CVE seja
+atribuído para um problema confirmado, ele pode entrar em contato com a
+:doc:`kernel CVE assignment team<../process/cve>` para obter um.
+
+Acordo de não divulgação
+------------------------
+
+A equipe de segurança do kernel Linux não é um òrgão formal e, portanto, é
+incapaz de celebrar quaisquer acordos de não divulgação.
--
2.55.0
^ permalink raw reply related
* [PATCH v12 4/4] HID: hid-msi: Add Rumble Intensity Attributes
From: Derek J. Clark @ 2026-07-14 10:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260714102640.18075-1-derekjohn.clark@gmail.com>
Adds intensity adjustment for the left and right rumble motors.
Claude was used during the reverse-engineering data gathering for this
feature done by Zhouwang Huang. As the code had already been affected,
I used Claude to create the initial framing for the feature, then did
manual cleanup of the _show and _store functions afterwards to fix bugs
and keep the coding style consistent. Claude was also used as an initial
reviewer of this patch.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Link: https://patch.msgid.link/20260529072111.7565-5-derekjohn.clark@gmail.com
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v12:
- On address mismatch, assume stale message return and keep waiting for
correct message.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
v11:
- Restore dropped changes from v10.
v7:
- Match on write address for rumble reports to prevent late ACK
from causing synchronization errors.
- Use spinlock for read/write profile_pending.
- Use smp_[store_release|load_acquire] pattern for checking
gamepad_registered to avoid possible races during teardown.
- Use struct for rumble reports.
v6:
- Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
config.
- Add spinlock_irqsave for read/write access on rumble_intensity
variables.
- Gate all attribute show/store functions with gamepad_registered.
v5:
- Remove mkey related changes.
v2:
- Use pending_profile and sync to rom mutexes.
---
drivers/hid/hid-msi.c | 202 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 201 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index ddb64adb08b9..b78e5bd1cc9e 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -79,6 +79,8 @@ enum claw_profile_ack_pending {
CLAW_M1_PENDING,
CLAW_M2_PENDING,
CLAW_RGB_PENDING,
+ CLAW_RUMBLE_LEFT_PENDING,
+ CLAW_RUMBLE_RIGHT_PENDING,
};
enum claw_key_index {
@@ -271,6 +273,11 @@ static const u16 button_mapping_addr_new[] = {
static const u16 rgb_addr_old = 0x01fa;
static const u16 rgb_addr_new = 0x024a;
+static const u16 rumble_addr[] = {
+ 0x0022, /* left */
+ 0x0023, /* right */
+};
+
struct claw_command_report {
u8 report_id;
u8 padding[2];
@@ -313,6 +320,12 @@ struct claw_rgb_report {
struct rgb_frame zone_data;
} __packed;
+struct claw_rumble_report {
+ struct claw_profile_report;
+ u8 padding;
+ u8 intensity;
+} __packed;
+
struct claw_drvdata {
/* MCU General Variables */
enum claw_profile_ack_pending profile_pending;
@@ -338,8 +351,12 @@ struct claw_drvdata {
enum claw_gamepad_mode_index gamepad_mode;
u8 m1_codes[CLAW_KEYS_MAX];
u8 m2_codes[CLAW_KEYS_MAX];
- spinlock_t mode_lock; /* Lock for mode data read/write */
+ u8 rumble_intensity_right;
+ u8 rumble_intensity_left;
const u16 *bmap_addr;
+ spinlock_t rumble_lock; /* lock for rumble_intensity read/write */
+ spinlock_t mode_lock; /* Lock for mode data read/write */
+ bool rumble_support;
bool gp_registered;
bool bmap_support;
@@ -388,6 +405,7 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
{
enum claw_profile_ack_pending profile;
+ struct claw_rumble_report *rumble;
struct claw_mkey_report *mkeys;
struct claw_rgb_report *frame;
u16 rgb_addr, read_addr;
@@ -441,6 +459,20 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
}
break;
+ case CLAW_RUMBLE_LEFT_PENDING:
+ rumble = (struct claw_rumble_report *)cmd_rep->data;
+ if (be16_to_cpu(rumble->read_addr) != rumble_addr[0])
+ return -EAGAIN;
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ drvdata->rumble_intensity_left = rumble->intensity;
+ break;
+ case CLAW_RUMBLE_RIGHT_PENDING:
+ rumble = (struct claw_rumble_report *)cmd_rep->data;
+ if (be16_to_cpu(rumble->read_addr) != rumble_addr[1])
+ return -EAGAIN;
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ drvdata->rumble_intensity_right = rumble->intensity;
+ break;
default:
dev_dbg(&drvdata->hdev->dev,
"Got profile event without changes pending from command: %x\n",
@@ -988,6 +1020,162 @@ static ssize_t button_mapping_options_show(struct device *dev,
}
static DEVICE_ATTR_RO(button_mapping_options);
+static ssize_t rumble_intensity_left_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[0])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 100)
+ return -EINVAL;
+
+ report.intensity = val;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t rumble_intensity_left_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[0])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
+ u8 val;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ val = drvdata->rumble_intensity_left;
+
+ return sysfs_emit(buf, "%u\n", val);
+}
+static DEVICE_ATTR_RW(rumble_intensity_left);
+
+static ssize_t rumble_intensity_right_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[1])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 100)
+ return -EINVAL;
+
+ report.intensity = val;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t rumble_intensity_right_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[1])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
+ u8 val;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ val = drvdata->rumble_intensity_right;
+
+ return sysfs_emit(buf, "%u\n", val);
+}
+static DEVICE_ATTR_RW(rumble_intensity_right);
+
+static ssize_t rumble_intensity_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0-100\n");
+}
+static DEVICE_ATTR_RO(rumble_intensity_range);
+
static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
int n)
{
@@ -1008,6 +1196,12 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu
attr == &dev_attr_reset.attr)
return attr->mode;
+ /* Hide rumble attrs if not supported */
+ if (attr == &dev_attr_rumble_intensity_left.attr ||
+ attr == &dev_attr_rumble_intensity_right.attr ||
+ attr == &dev_attr_rumble_intensity_range.attr)
+ return drvdata->rumble_support ? attr->mode : 0;
+
/* Hide button mapping attrs if it isn't supported */
return drvdata->bmap_support ? attr->mode : 0;
}
@@ -1021,6 +1215,9 @@ static struct attribute *claw_gamepad_attrs[] = {
&dev_attr_mkeys_function.attr,
&dev_attr_mkeys_function_index.attr,
&dev_attr_reset.attr,
+ &dev_attr_rumble_intensity_left.attr,
+ &dev_attr_rumble_intensity_right.attr,
+ &dev_attr_rumble_intensity_range.attr,
NULL,
};
@@ -1600,6 +1797,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
drvdata->bmap_support = true;
if (minor >= 0x66) {
drvdata->bmap_addr = button_mapping_addr_new;
+ drvdata->rumble_support = true;
drvdata->rgb_addr = rgb_addr_new;
} else {
drvdata->bmap_addr = button_mapping_addr_old;
@@ -1611,6 +1809,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
drvdata->bmap_support = true;
drvdata->bmap_addr = button_mapping_addr_new;
+ drvdata->rumble_support = true;
drvdata->rgb_addr = rgb_addr_new;
return;
}
@@ -1661,6 +1860,7 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
spin_lock_init(&drvdata->mode_lock);
spin_lock_init(&drvdata->profile_lock);
spin_lock_init(&drvdata->frame_lock);
+ spin_lock_init(&drvdata->rumble_lock);
init_completion(&drvdata->send_cmd_complete);
INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
--
2.54.0
^ permalink raw reply related
* [PATCH v12 3/4] HID: hid-msi: Add RGB control interface
From: Derek J. Clark @ 2026-07-14 10:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260714102640.18075-1-derekjohn.clark@gmail.com>
Adds RGB control interface for MSI Claw devices. The MSI Claw uses a
fairly unique RGB interface. It has 9 total zones (4 per joystick ring
and 1 for the ABXY buttons), and supports up to 8 sequential frames of
RGB zone data. Each frame is written to a specific area of MCU memory by
the profile command, the value of which changes based on the firmware of
the device. Unlike other devices (such as the Legion Go or the OneXPlayer
devices), there are no hard coded effects built into the MCU. Instead,
the basic effects are provided as a series of frame data. I have
mirrored the effects available in Windows in this driver, while keeping
the effect names consistent with the Lenovo drivers for the effects that
are similar.
Initial reverse-engineering and implementation of this feature was done
by Zhouwang Huang. I refactored the overall format to conform to kernel
driver best practices and style guides. Claude was used as an initial
reviewer of this patch.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Link: https://patch.msgid.link/20260529072111.7565-4-derekjohn.clark@gmail.com
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v12:
- On remove, cancel the rgb_queue last to avoid a re-arming, and in
rgb_queue_fn check rgb_registered to avoid running after the sysfs
group has been removed.
- On address mismatch, assume stale message return and keep waiting for
correct message.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
- Add note to led_cdev.name to (hopefully) silence sashiko-bot about
an impossible double device collision.
- Explicitly check if the current address is less than the rgb_address
to avoid invalid overflow math.
v9:
- Limit guard usage in cfg_setup_fn to avoid holding a lock during
registration and add group events.
- Don't use devm_ functions in cfg_setup_fn. Manually setup/teardown
devices and groups to prevent use after free.
- Ensure rgb_queue work is canceled during suspend.
- Check for drvdata in rgb_queue_fn to avoid use after free during
teardown.
v8:
- Ensure led_classdev is unregistered if adding attribute group fails.
- Reorder remove actions to ensure no use-after free or rearming cleared
flags.
v7:
- Use smp_[store_release|load_acquire] pattern for checking
rgb_registered to avoid possible races during teardown.
- Add gating to cfg_setup_fn, allowing either gamepad settings or rgb
settings to populate if the other fails for any reason.
- Use spinlock when writing profile_pending.
v6:
- Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
config.
- Gate all attribute show/store functions with rgb_registered,
enabling use of devm_device_add_group.
v5:
- Move adding the RGB device into cfg_setup to prevent led core
attributes from being written to prior to setup completing.
- Ensure frame_lock is properly init.
- Change variable names in RGB functions from frame and zone to f and
z respectively to fit all scoped_guard actions in 100 columns.
v4:
- Fix frame_calc validity check to use >=.
- USe spinlock instead of mutex in raw_event and related attribute
_store function.
- Ensure delayed work is canceled in suspend & canceled before sysfs
attribute removal.
v3:
- Add mutex for read/write of rgb frame data.
- Remove setting rgb_frame_count when reading rgb profiles as it always
returns garbage data.
- Ensure rgb_speed is getting drvdata from a valid lookup (not hdev).
v2:
- Use pending_profile mutex
---
drivers/hid/hid-msi.c | 679 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 664 insertions(+), 15 deletions(-)
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index 9d3249fa5029..ddb64adb08b9 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -21,6 +21,7 @@
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/kobject.h>
+#include <linux/led-class-multicolor.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/mutex.h>
@@ -44,6 +45,10 @@
#define CLAW_KEYS_MAX 5
+#define CLAW_RGB_ZONES 9
+#define CLAW_RGB_MAX_FRAMES 8
+#define CLAW_RGB_FRAME_OFFSET 0x24
+
enum claw_command_index {
CLAW_COMMAND_TYPE_NONE = 0x00,
CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
@@ -73,6 +78,7 @@ enum claw_profile_ack_pending {
CLAW_NO_PENDING,
CLAW_M1_PENDING,
CLAW_M2_PENDING,
+ CLAW_RGB_PENDING,
};
enum claw_key_index {
@@ -236,6 +242,22 @@ static const struct {
{ 0xff, "DISABLED" },
};
+enum claw_rgb_effect_index {
+ CLAW_RGB_EFFECT_MONOCOLOR,
+ CLAW_RGB_EFFECT_BREATHE,
+ CLAW_RGB_EFFECT_CHROMA,
+ CLAW_RGB_EFFECT_RAINBOW,
+ CLAW_RGB_EFFECT_FROSTFIRE,
+};
+
+static const char * const claw_rgb_effect_text[] = {
+ [CLAW_RGB_EFFECT_MONOCOLOR] = "monocolor",
+ [CLAW_RGB_EFFECT_BREATHE] = "breathe",
+ [CLAW_RGB_EFFECT_CHROMA] = "chroma",
+ [CLAW_RGB_EFFECT_RAINBOW] = "rainbow",
+ [CLAW_RGB_EFFECT_FROSTFIRE] = "frostfire",
+};
+
static const u16 button_mapping_addr_old[] = {
0x007a, /* M1 */
0x011f, /* M2 */
@@ -246,6 +268,9 @@ static const u16 button_mapping_addr_new[] = {
0x0164, /* M2 */
};
+static const u16 rgb_addr_old = 0x01fa;
+static const u16 rgb_addr_new = 0x024a;
+
struct claw_command_report {
u8 report_id;
u8 padding[2];
@@ -267,6 +292,27 @@ struct claw_mkey_report {
u8 codes[5];
} __packed;
+struct rgb_zone {
+ u8 red;
+ u8 green;
+ u8 blue;
+};
+
+struct rgb_frame {
+ struct rgb_zone zone[CLAW_RGB_ZONES];
+};
+
+struct claw_rgb_report {
+ struct claw_profile_report;
+ u8 frame_bytes;
+ u8 padding;
+ u8 frame_count;
+ u8 state; /* Always 0x09 */
+ u8 speed;
+ u8 brightness;
+ struct rgb_frame zone_data;
+} __packed;
+
struct claw_drvdata {
/* MCU General Variables */
enum claw_profile_ack_pending profile_pending;
@@ -292,10 +338,22 @@ struct claw_drvdata {
enum claw_gamepad_mode_index gamepad_mode;
u8 m1_codes[CLAW_KEYS_MAX];
u8 m2_codes[CLAW_KEYS_MAX];
- const u16 *bmap_addr;
spinlock_t mode_lock; /* Lock for mode data read/write */
+ const u16 *bmap_addr;
bool gp_registered;
bool bmap_support;
+
+ /* RGB Variables */
+ struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES];
+ enum claw_rgb_effect_index rgb_effect;
+ struct led_classdev_mc led_mc;
+ struct delayed_work rgb_queue;
+ spinlock_t frame_lock; /* lock for rgb_frames read/write */
+ bool rgb_registered;
+ u8 rgb_frame_count;
+ bool rgb_enabled;
+ u8 rgb_speed;
+ u16 rgb_addr;
};
static int get_endpoint_address(struct hid_device *hdev)
@@ -331,7 +389,10 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
{
enum claw_profile_ack_pending profile;
struct claw_mkey_report *mkeys;
- u8 *codes, key;
+ struct claw_rgb_report *frame;
+ u16 rgb_addr, read_addr;
+ u8 *codes, key, f_idx;
+ u16 frame_calc;
int i;
scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
@@ -348,6 +409,38 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
for (i = 0; i < CLAW_KEYS_MAX; i++)
codes[i] = (mkeys->codes[i]);
break;
+ case CLAW_RGB_PENDING:
+ frame = (struct claw_rgb_report *)cmd_rep->data;
+ rgb_addr = drvdata->rgb_addr;
+ read_addr = be16_to_cpu(frame->read_addr);
+
+ if (read_addr < drvdata->rgb_addr)
+ return -EAGAIN;
+
+ frame_calc = (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET;
+ if (frame_calc >= CLAW_RGB_MAX_FRAMES) {
+ dev_err(&drvdata->hdev->dev, "Got unsupported frame index: %x\n",
+ frame_calc);
+ return -EAGAIN;
+ }
+ f_idx = frame_calc;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data,
+ sizeof(struct rgb_frame));
+
+ /* Only use frame 0 for remaining variable assignment */
+ if (f_idx != 0)
+ break;
+
+ drvdata->rgb_speed = frame->speed;
+ drvdata->led_mc.led_cdev.brightness = frame->brightness;
+ drvdata->led_mc.subled_info[0].intensity = frame->zone_data.zone[0].red;
+ drvdata->led_mc.subled_info[1].intensity = frame->zone_data.zone[0].green;
+ drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue;
+ }
+
+ break;
default:
dev_dbg(&drvdata->hdev->dev,
"Got profile event without changes pending from command: %x\n",
@@ -936,32 +1029,553 @@ static const struct attribute_group claw_gamepad_attr_group = {
.is_visible = claw_gamepad_attr_is_visible,
};
+/* Read RGB config from device */
+static int claw_read_rgb_config(struct hid_device *hdev)
+{
+ u8 data[4] = { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET };
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u16 read_addr = drvdata->rgb_addr;
+ size_t len = ARRAY_SIZE(data);
+ int ret, i;
+
+ if (!drvdata->rgb_addr)
+ return -ENODEV;
+
+ /* Loop through all 8 pages of RGB data */
+ guard(mutex)(&drvdata->profile_mutex);
+ for (i = 0; i < CLAW_RGB_MAX_FRAMES; i++) {
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RGB_PENDING;
+ data[1] = (read_addr >> 8) & 0xff;
+ data[2] = read_addr & 0x00ff;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 25);
+ if (ret)
+ return ret;
+
+ read_addr += CLAW_RGB_FRAME_OFFSET;
+ }
+
+ return 0;
+}
+
+/* Send RGB configuration to device */
+static int claw_write_rgb_state(struct claw_drvdata *drvdata)
+{
+ struct claw_rgb_report report = { {0x01, 0}, CLAW_RGB_FRAME_OFFSET, 0x00,
+ drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed,
+ drvdata->led_mc.led_cdev.brightness };
+ u16 write_addr = drvdata->rgb_addr;
+ int f, ret;
+
+ if (!drvdata->rgb_addr)
+ return -ENODEV;
+
+ if (!drvdata->rgb_frame_count)
+ return -EINVAL;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ /* Loop through (up to) 8 pages of RGB data */
+ for (f = 0; f < drvdata->rgb_frame_count; f++) {
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock)
+ report.zone_data = drvdata->rgb_frames[f];
+
+ /* Set the MCU address to write the frame data to */
+ report.read_addr = cpu_to_be16(write_addr);
+
+ /* Serialize the rgb_report and write it to MCU */
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* Increment the write addr by the offset for the next frame */
+ write_addr += CLAW_RGB_FRAME_OFFSET;
+ }
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+
+ return ret;
+}
+
+/* Fill all zones with the same color */
+static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone zone)
+{
+ int z;
+
+ for (z = 0; z < CLAW_RGB_ZONES; z++)
+ frame->zone[z] = zone;
+}
+
+/* Apply solid effect (1 frame, no color) */
+static int claw_apply_disabled(struct claw_drvdata *drvdata)
+{
+ struct rgb_zone off = { 0x00, 0x00, 0x00};
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 1;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], off);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply solid effect (1 frame, all zones same color) */
+static int claw_apply_monocolor(struct claw_drvdata *drvdata)
+{
+ struct mc_subled *subleds = drvdata->led_mc.subled_info;
+ struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
+ subleds[2].intensity };
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 1;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply breathe effect (2 frames: color -> off) */
+static int claw_apply_breathe(struct claw_drvdata *drvdata)
+{
+ struct mc_subled *subleds = drvdata->led_mc.subled_info;
+ struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
+ subleds[2].intensity };
+ static const struct rgb_zone off = { 0, 0, 0 };
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 2;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
+ claw_frame_fill_solid(&drvdata->rgb_frames[1], off);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */
+static int claw_apply_chroma(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* red */
+ {255, 255, 0}, /* yellow */
+ { 0, 255, 0}, /* green */
+ { 0, 255, 255}, /* cyan */
+ { 0, 0, 255}, /* blue */
+ {255, 0, 255}, /* magenta */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++)
+ claw_frame_fill_solid(&drvdata->rgb_frames[f], colors[f]);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply rainbow effect (4 frames: rotating colors around joysticks) */
+static int claw_apply_rainbow(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* red */
+ { 0, 255, 0}, /* green */
+ { 0, 255, 255}, /* cyan */
+ { 0, 0, 255}, /* blue */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f, z;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++) {
+ for (z = 0; z < 4; z++) {
+ drvdata->rgb_frames[f].zone[z] = colors[(z + f) % 4];
+ drvdata->rgb_frames[f].zone[z + 4] = colors[(z + f) % 4];
+ }
+ drvdata->rgb_frames[f].zone[8] = colors[f];
+ }
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/*
+ * Apply frostfire effect (4 frames: fire vs ice rotating)
+ * Right joystick: fire red -> dark -> ice blue -> dark (clockwise)
+ * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise)
+ * ABXY: fire red -> dark -> ice blue -> dark
+ */
+static int claw_apply_frostfire(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* fire red */
+ { 0, 0, 0}, /* dark */
+ { 0, 0, 255}, /* ice blue */
+ { 0, 0, 0}, /* dark */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f, z;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++) {
+ for (z = 0; z < 4; z++) {
+ drvdata->rgb_frames[f].zone[z] = colors[(z + f) % 4];
+ drvdata->rgb_frames[f].zone[z + 4] = colors[(z - f + 6) % 4];
+ }
+ drvdata->rgb_frames[f].zone[8] = colors[f];
+ }
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply current state to device */
+static int claw_apply_rgb_state(struct claw_drvdata *drvdata)
+{
+ if (!drvdata->rgb_enabled)
+ return claw_apply_disabled(drvdata);
+
+ switch (drvdata->rgb_effect) {
+ case CLAW_RGB_EFFECT_MONOCOLOR:
+ return claw_apply_monocolor(drvdata);
+ case CLAW_RGB_EFFECT_BREATHE:
+ return claw_apply_breathe(drvdata);
+ case CLAW_RGB_EFFECT_CHROMA:
+ return claw_apply_chroma(drvdata);
+ case CLAW_RGB_EFFECT_RAINBOW:
+ return claw_apply_rainbow(drvdata);
+ case CLAW_RGB_EFFECT_FROSTFIRE:
+ return claw_apply_frostfire(drvdata);
+ default:
+ dev_err(drvdata->led_mc.led_cdev.dev,
+ "No supported rgb_effect selected\n");
+ return -EINVAL;
+ }
+}
+
+static void claw_rgb_queue_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, rgb_queue);
+ int ret;
+
+ if (!drvdata)
+ return;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return;
+ }
+
+ ret = claw_apply_rgb_state(drvdata);
+ if (ret)
+ dev_err(drvdata->led_mc.led_cdev.dev,
+ "Failed to apply RGB state: %d\n", ret);
+}
+
+static ssize_t effect_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = sysfs_match_string(claw_rgb_effect_text, buf);
+ if (ret < 0)
+ return ret;
+
+ drvdata->rgb_effect = ret;
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t effect_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ if (drvdata->rgb_effect >= ARRAY_SIZE(claw_rgb_effect_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]);
+}
+
+static DEVICE_ATTR_RW(effect);
+
+static ssize_t effect_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(effect_index);
+
+static ssize_t enabled_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ bool val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+
+ drvdata->rgb_enabled = val;
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t enabled_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false");
+}
+static DEVICE_ATTR_RW(enabled);
+
+static ssize_t enabled_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "true false\n");
+}
+static DEVICE_ATTR_RO(enabled_index);
+
+static ssize_t speed_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ unsigned int val, speed;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtouint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 20)
+ return -EINVAL;
+
+ /* 0 is fastest, invert value for intuitive userspace speed */
+ speed = 20 - val;
+
+ drvdata->rgb_speed = speed;
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t speed_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ u8 speed = 20 - drvdata->rgb_speed;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ return sysfs_emit(buf, "%u\n", speed);
+}
+static DEVICE_ATTR_RW(speed);
+
+static ssize_t speed_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "0-20\n");
+}
+static DEVICE_ATTR_RO(speed_range);
+
+static void claw_led_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness _brightness)
+{
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return;
+ }
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+}
+
+static struct attribute *claw_rgb_attrs[] = {
+ &dev_attr_effect.attr,
+ &dev_attr_effect_index.attr,
+ &dev_attr_enabled.attr,
+ &dev_attr_enabled_index.attr,
+ &dev_attr_speed.attr,
+ &dev_attr_speed_range.attr,
+ NULL,
+};
+
+static const struct attribute_group claw_rgb_attr_group = {
+ .attrs = claw_rgb_attrs,
+};
+
+static struct mc_subled claw_rgb_subled_info[] = {
+ {
+ .color_index = LED_COLOR_ID_RED,
+ .channel = 0x1,
+ },
+ {
+ .color_index = LED_COLOR_ID_GREEN,
+ .channel = 0x2,
+ },
+ {
+ .color_index = LED_COLOR_ID_BLUE,
+ .channel = 0x3,
+ },
+};
+
static void cfg_setup_fn(struct work_struct *work)
{
struct delayed_work *dwork = container_of(work, struct delayed_work, work);
struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup);
+ bool gamepad_ready = false, rgb_ready = false, gp_registered, rgb_registered;
int ret;
ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE,
NULL, 0, 25);
if (ret) {
dev_err(&drvdata->hdev->dev,
- "Failed to setup device, can't read gamepad mode: %d\n", ret);
- return;
+ "Failed to read gamepad mode: %d\n", ret);
+ goto prep_rgb;
}
+ gamepad_ready = true;
- /* Add sysfs attributes after we get the device state */
- ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+prep_rgb:
+ ret = claw_read_rgb_config(drvdata->hdev);
if (ret) {
dev_err(&drvdata->hdev->dev,
- "Failed to setup device, can't create gamepad attrs: %d\n", ret);
- return;
+ "Failed to read RGB config: %d\n", ret);
+ goto try_gamepad;
}
+ rgb_ready = true;
+
+ /* Add sysfs attributes after we get the device state */
+try_gamepad:
scoped_guard(spinlock_irqsave, &drvdata->registration_lock)
- /* Pairs with smp_load_acquire in attribute show/store functions */
- smp_store_release(&drvdata->gp_registered, true);
+ /* Pairs with smp_store_release from below */
+ gp_registered = smp_load_acquire(&drvdata->gp_registered);
+
+ if (!gp_registered && gamepad_ready) {
+ ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to create gamepad attrs: %d\n", ret);
+ goto try_rgb;
+ }
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->gp_registered, true);
+ gp_registered = true;
+ }
+ }
+
+try_rgb:
+ /* Add and enable RGB interface once we have the device state */
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock)
+ /* Pairs with smp_store_release from below */
+ rgb_registered = smp_load_acquire(&drvdata->rgb_registered);
+
+ if (!rgb_registered && rgb_ready) {
+ ret = led_classdev_multicolor_register(&drvdata->hdev->dev,
+ &drvdata->led_mc);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to create led device: %d\n", ret);
+ goto update_kobjects;
+ }
+
+ ret = device_add_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to create RGB attrs: %d\n", ret);
+ led_classdev_multicolor_unregister(&drvdata->led_mc);
+ goto update_kobjects;
+ }
- kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->rgb_registered, true);
+ rgb_registered = true;
+ }
+ }
+
+update_kobjects:
+ if (gp_registered)
+ kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+ if (rgb_registered)
+ kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE);
}
static void cfg_resume_fn(struct work_struct *work)
@@ -970,8 +1584,10 @@ static void cfg_resume_fn(struct work_struct *work)
struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume);
guard(spinlock_irqsave)(&drvdata->registration_lock);
- /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
- if (!smp_load_acquire(&drvdata->gp_registered))
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered) ||
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ !smp_load_acquire(&drvdata->rgb_registered))
schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
}
@@ -982,18 +1598,24 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
if (major == 0x01) {
drvdata->bmap_support = true;
- if (minor >= 0x66)
+ if (minor >= 0x66) {
drvdata->bmap_addr = button_mapping_addr_new;
- else
+ drvdata->rgb_addr = rgb_addr_new;
+ } else {
drvdata->bmap_addr = button_mapping_addr_old;
+ drvdata->rgb_addr = rgb_addr_old;
+ }
return;
}
if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
drvdata->bmap_support = true;
drvdata->bmap_addr = button_mapping_addr_new;
+ drvdata->rgb_addr = rgb_addr_new;
return;
}
+
+ drvdata->rgb_addr = rgb_addr_old;
}
static int claw_probe(struct hid_device *hdev, u8 ep)
@@ -1008,6 +1630,7 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
return -ENOMEM;
drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
+ drvdata->rgb_enabled = true;
drvdata->hdev = hdev;
drvdata->ep = ep;
@@ -1018,6 +1641,18 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
if (!drvdata->bmap_support)
dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n");
+ /* Device is hardwired and name is guaranteed to be unique */
+ drvdata->led_mc.led_cdev.name = "msi_claw:rgb:joystick_rings";
+ drvdata->led_mc.led_cdev.brightness = 0x50;
+ drvdata->led_mc.led_cdev.max_brightness = 0x64;
+ drvdata->led_mc.led_cdev.color = LED_COLOR_ID_RGB;
+ drvdata->led_mc.led_cdev.brightness_set = claw_led_brightness_set;
+ drvdata->led_mc.num_colors = 3;
+ drvdata->led_mc.subled_info = devm_kmemdup(&hdev->dev, claw_rgb_subled_info,
+ sizeof(claw_rgb_subled_info), GFP_KERNEL);
+ if (!drvdata->led_mc.subled_info)
+ return -ENOMEM;
+
mutex_init(&drvdata->cfg_mutex);
mutex_init(&drvdata->profile_mutex);
mutex_init(&drvdata->rom_mutex);
@@ -1025,9 +1660,11 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
spin_lock_init(&drvdata->cmd_lock);
spin_lock_init(&drvdata->mode_lock);
spin_lock_init(&drvdata->profile_lock);
+ spin_lock_init(&drvdata->frame_lock);
init_completion(&drvdata->send_cmd_complete);
INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
+ INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn);
/* For control interface: open the HID transport for sending commands. */
ret = hid_hw_open(hdev);
@@ -1084,6 +1721,7 @@ static void claw_remove(struct hid_device *hdev)
{
struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
bool gp_registered;
+ bool rgb_registered;
if (!drvdata)
return;
@@ -1096,11 +1734,21 @@ static void claw_remove(struct hid_device *hdev)
gp_registered = smp_load_acquire(&drvdata->gp_registered);
/* Pairs with smp_load_acquire in attribute show/store functions */
smp_store_release(&drvdata->gp_registered, false);
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ rgb_registered = smp_load_acquire(&drvdata->rgb_registered);
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->rgb_registered, false);
}
if (gp_registered)
device_remove_group(&hdev->dev, &claw_gamepad_attr_group);
+ if (rgb_registered) {
+ device_remove_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
+ led_classdev_multicolor_unregister(&drvdata->led_mc);
+ }
+ cancel_delayed_work_sync(&drvdata->rgb_queue);
+
hid_hw_close(hdev);
}
@@ -1160,6 +1808,7 @@ static int claw_suspend(struct hid_device *hdev)
cancel_delayed_work_sync(&drvdata->cfg_resume);
cancel_delayed_work_sync(&drvdata->cfg_setup);
+ cancel_delayed_work_sync(&drvdata->rgb_queue);
return 0;
}
--
2.54.0
^ permalink raw reply related
* [PATCH v12 2/4] HID: hid-msi: Add M-key mapping attributes
From: Derek J. Clark @ 2026-07-14 10:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260714102640.18075-1-derekjohn.clark@gmail.com>
Adds attributes that allow for remapping the M-keys with up to 5 values
when in macro mode. There are 2 mappable buttons on the rear of the
device, M1 on the right and M2 on the left. When mapped, the events will
fire from one of three event devices: gamepad buttons will fire from the
device handled by xpad, while keyboard and mouse events will fire from
respectively typed evdevs provided by the input core. Names of each
mapping have been kept as close to the event that will fire from the evdev
as possible, with context added to the ABS_ events on the direction of the
movement.
Initial reverse-engineering and implementation of this feature was done
by Zhouwang Huang. I refactored the overall format to conform to kernel
driver best practices and style guides. Claude was used as an initial
reviewer of this patch.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Link: https://patch.msgid.link/20260529072111.7565-3-derekjohn.clark@gmail.com
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v12:
- On address mismatch, assume stale message return and keep waiting for
correct message.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
v10:
- Remove additional gamepad_registered variable left over after rename
to gp_registered.
v8:
- Wrap all branches under single cmd_lock guard in claw_raw_event.
- Reject generic ACK in claw_raw_event if waiting_cmd is for another
branch.
v7:
- Use smp_[store_release|load_acquire] pattern for checking
gamepad_registered to avoid possible races during teardown.
- Add profile_lock for read/write profile_pending.
- Match on write address for mkey reports to prevent late ACK
from causing synchronization errors.
- Use struct for mkey reports.
v6:
- Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
config.
- Gate all attribute show/store functions with gamepad_registered.
- Remove duplicated argv_free macro.
v5:
- Ensure adding "DISABLED" key to valid entries is done in the correct
patch.
- Re-enable sending an empty string to clear button mappings in
addition to setting DISABLED.
v4:
- Change dev_warn to dev_dbg in claw_profile_event.
- use __free with DEFINE_FREE macro for argv instead of manually
running argv_free, cleaining up scoped_guard goto.
v3:
- Use scoped_guard where necessary.
v2:
- Add mutex for SYNC_TO_ROM commands to ensure every SYNC is completed
before more data is written to the MCU volatile memory.
- Add mutex for profile_pending to ensure every profile action
response is serialized to the generating command.
---
drivers/hid/hid-msi.c | 451 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 449 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index 8a48ce099c1c..9d3249fa5029 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -42,6 +42,8 @@
#define CLAW_DINPUT_CFG_INTF_IN 0x82
#define CLAW_XINPUT_CFG_INTF_IN 0x83
+#define CLAW_KEYS_MAX 5
+
enum claw_command_index {
CLAW_COMMAND_TYPE_NONE = 0x00,
CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
@@ -67,6 +69,17 @@ static const char * const claw_gamepad_mode_text[] = {
[CLAW_GAMEPAD_MODE_DESKTOP] = "desktop",
};
+enum claw_profile_ack_pending {
+ CLAW_NO_PENDING,
+ CLAW_M1_PENDING,
+ CLAW_M2_PENDING,
+};
+
+enum claw_key_index {
+ CLAW_KEY_M1,
+ CLAW_KEY_M2,
+};
+
enum claw_mkeys_function_index {
CLAW_MKEY_FUNCTION_MACRO,
CLAW_MKEY_FUNCTION_DISABLED,
@@ -84,6 +97,155 @@ static const char * const claw_mkeys_function_text[] = {
[CLAW_MKEY_FUNCTION_COMBO] = "combination",
};
+static const struct {
+ u8 code;
+ const char *name;
+} claw_button_mapping_key_map[] = {
+ /* Gamepad buttons */
+ { 0x01, "ABS_HAT0Y_UP" },
+ { 0x02, "ABS_HAT0Y_DOWN" },
+ { 0x03, "ABS_HAT0X_LEFT" },
+ { 0x04, "ABS_HAT0X_RIGHT" },
+ { 0x05, "BTN_TL" },
+ { 0x06, "BTN_TR" },
+ { 0x07, "BTN_THUMBL" },
+ { 0x08, "BTN_THUMBR" },
+ { 0x09, "BTN_SOUTH" },
+ { 0x0a, "BTN_EAST" },
+ { 0x0b, "BTN_NORTH" },
+ { 0x0c, "BTN_WEST" },
+ { 0x0d, "BTN_MODE" },
+ { 0x0e, "BTN_SELECT" },
+ { 0x0f, "BTN_START" },
+ { 0x13, "BTN_TL2"},
+ { 0x14, "BTN_TR2"},
+ { 0x15, "ABS_Y_UP"},
+ { 0x16, "ABS_Y_DOWN"},
+ { 0x17, "ABS_X_LEFT"},
+ { 0x18, "ABS_X_RIGHT"},
+ { 0x19, "ABS_RY_UP"},
+ { 0x1a, "ABS_RY_DOWN"},
+ { 0x1b, "ABS_RX_LEFT"},
+ { 0x1c, "ABS_RX_RIGHT"},
+ /* Keyboard keys */
+ { 0x32, "KEY_ESC" },
+ { 0x33, "KEY_F1" },
+ { 0x34, "KEY_F2" },
+ { 0x35, "KEY_F3" },
+ { 0x36, "KEY_F4" },
+ { 0x37, "KEY_F5" },
+ { 0x38, "KEY_F6" },
+ { 0x39, "KEY_F7" },
+ { 0x3a, "KEY_F8" },
+ { 0x3b, "KEY_F9" },
+ { 0x3c, "KEY_F10" },
+ { 0x3d, "KEY_F11" },
+ { 0x3e, "KEY_F12" },
+ { 0x3f, "KEY_GRAVE" },
+ { 0x40, "KEY_1" },
+ { 0x41, "KEY_2" },
+ { 0x42, "KEY_3" },
+ { 0x43, "KEY_4" },
+ { 0x44, "KEY_5" },
+ { 0x45, "KEY_6" },
+ { 0x46, "KEY_7" },
+ { 0x47, "KEY_8" },
+ { 0x48, "KEY_9" },
+ { 0x49, "KEY_0" },
+ { 0x4a, "KEY_MINUS" },
+ { 0x4b, "KEY_EQUAL" },
+ { 0x4c, "KEY_BACKSPACE" },
+ { 0x4d, "KEY_TAB" },
+ { 0x4e, "KEY_Q" },
+ { 0x4f, "KEY_W" },
+ { 0x50, "KEY_E" },
+ { 0x51, "KEY_R" },
+ { 0x52, "KEY_T" },
+ { 0x53, "KEY_Y" },
+ { 0x54, "KEY_U" },
+ { 0x55, "KEY_I" },
+ { 0x56, "KEY_O" },
+ { 0x57, "KEY_P" },
+ { 0x58, "KEY_LEFTBRACE" },
+ { 0x59, "KEY_RIGHTBRACE" },
+ { 0x5a, "KEY_BACKSLASH" },
+ { 0x5b, "KEY_CAPSLOCK" },
+ { 0x5c, "KEY_A" },
+ { 0x5d, "KEY_S" },
+ { 0x5e, "KEY_D" },
+ { 0x5f, "KEY_F" },
+ { 0x60, "KEY_G" },
+ { 0x61, "KEY_H" },
+ { 0x62, "KEY_J" },
+ { 0x63, "KEY_K" },
+ { 0x64, "KEY_L" },
+ { 0x65, "KEY_SEMICOLON" },
+ { 0x66, "KEY_APOSTROPHE" },
+ { 0x67, "KEY_ENTER" },
+ { 0x68, "KEY_LEFTSHIFT" },
+ { 0x69, "KEY_Z" },
+ { 0x6a, "KEY_X" },
+ { 0x6b, "KEY_C" },
+ { 0x6c, "KEY_V" },
+ { 0x6d, "KEY_B" },
+ { 0x6e, "KEY_N" },
+ { 0x6f, "KEY_M" },
+ { 0x70, "KEY_COMMA" },
+ { 0x71, "KEY_DOT" },
+ { 0x72, "KEY_SLASH" },
+ { 0x73, "KEY_RIGHTSHIFT" },
+ { 0x74, "KEY_LEFTCTRL" },
+ { 0x75, "KEY_LEFTMETA" },
+ { 0x76, "KEY_LEFTALT" },
+ { 0x77, "KEY_SPACE" },
+ { 0x78, "KEY_RIGHTALT" },
+ { 0x79, "KEY_RIGHTCTRL" },
+ { 0x7a, "KEY_INSERT" },
+ { 0x7b, "KEY_HOME" },
+ { 0x7c, "KEY_PAGEUP" },
+ { 0x7d, "KEY_DELETE" },
+ { 0x7e, "KEY_END" },
+ { 0x7f, "KEY_PAGEDOWN" },
+ { 0x8a, "KEY_KPENTER" },
+ { 0x8b, "KEY_KP0" },
+ { 0x8c, "KEY_KP1" },
+ { 0x8d, "KEY_KP2" },
+ { 0x8e, "KEY_KP3" },
+ { 0x8f, "KEY_KP4" },
+ { 0x90, "KEY_KP5" },
+ { 0x91, "KEY_KP6" },
+ { 0x92, "KEY_KP7" },
+ { 0x93, "KEY_KP8" },
+ { 0x94, "KEY_KP9" },
+ { 0x95, "MD_PLAY" },
+ { 0x96, "MD_STOP" },
+ { 0x97, "MD_NEXT" },
+ { 0x98, "MD_PREV" },
+ { 0x99, "MD_VOL_UP" },
+ { 0x9a, "MD_VOL_DOWN" },
+ { 0x9b, "MD_VOL_MUTE" },
+ { 0x9c, "KEY_F23" },
+ /* Mouse events */
+ { 0xc8, "BTN_LEFT" },
+ { 0xc9, "BTN_MIDDLE" },
+ { 0xca, "BTN_RIGHT" },
+ { 0xcb, "BTN_SIDE" },
+ { 0xcc, "BTN_EXTRA" },
+ { 0xcd, "REL_WHEEL_UP" },
+ { 0xce, "REL_WHEEL_DOWN" },
+ { 0xff, "DISABLED" },
+};
+
+static const u16 button_mapping_addr_old[] = {
+ 0x007a, /* M1 */
+ 0x011f, /* M2 */
+};
+
+static const u16 button_mapping_addr_new[] = {
+ 0x00bb, /* M1 */
+ 0x0164, /* M2 */
+};
+
struct claw_command_report {
u8 report_id;
u8 padding[2];
@@ -92,26 +254,48 @@ struct claw_command_report {
u8 data[59];
} __packed;
+struct claw_profile_report {
+ u8 profile;
+ __be16 read_addr;
+} __packed;
+
+struct claw_mkey_report {
+ struct claw_profile_report;
+ u8 padding_0;
+ u8 padding_1;
+ u8 padding_2;
+ u8 codes[5];
+} __packed;
+
struct claw_drvdata {
/* MCU General Variables */
+ enum claw_profile_ack_pending profile_pending;
struct completion send_cmd_complete;
struct delayed_work cfg_resume;
struct delayed_work cfg_setup;
spinlock_t registration_lock; /* Lock for registration read/write */
+ struct mutex profile_mutex; /* mutex for profile_pending calls */
+ spinlock_t profile_lock; /* Lock for profile_pending read/write */
struct hid_device *hdev;
bool orphan_ack_pending;
struct mutex cfg_mutex; /* mutex for synchronous data */
+ struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */
bool waiting_for_ack;
spinlock_t cmd_lock; /* Lock for cmd data read/write */
u8 waiting_cmd;
int cmd_status;
+ u16 bcd_device;
u8 ep;
/* Gamepad Variables */
enum claw_mkeys_function_index mkeys_function;
enum claw_gamepad_mode_index gamepad_mode;
+ u8 m1_codes[CLAW_KEYS_MAX];
+ u8 m2_codes[CLAW_KEYS_MAX];
+ const u16 *bmap_addr;
spinlock_t mode_lock; /* Lock for mode data read/write */
bool gp_registered;
+ bool bmap_support;
};
static int get_endpoint_address(struct hid_device *hdev)
@@ -143,6 +327,39 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
return 0;
}
+static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
+{
+ enum claw_profile_ack_pending profile;
+ struct claw_mkey_report *mkeys;
+ u8 *codes, key;
+ int i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ profile = drvdata->profile_pending;
+
+ switch (profile) {
+ case CLAW_M1_PENDING:
+ case CLAW_M2_PENDING:
+ key = (profile == CLAW_M1_PENDING) ? CLAW_KEY_M1 : CLAW_KEY_M2;
+ mkeys = (struct claw_mkey_report *)cmd_rep->data;
+ if (be16_to_cpu(mkeys->read_addr) != drvdata->bmap_addr[key])
+ return -EAGAIN;
+ codes = (profile == CLAW_M1_PENDING) ? drvdata->m1_codes : drvdata->m2_codes;
+ for (i = 0; i < CLAW_KEYS_MAX; i++)
+ codes[i] = (mkeys->codes[i]);
+ break;
+ default:
+ dev_dbg(&drvdata->hdev->dev,
+ "Got profile event without changes pending from command: %x\n",
+ cmd_rep->cmd);
+ return -EINVAL;
+ }
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_NO_PENDING;
+
+ return 0;
+}
+
static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
u8 *data, int size)
{
@@ -171,9 +388,23 @@ static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *repor
complete(&drvdata->send_cmd_complete);
}
+ break;
+ case CLAW_COMMAND_TYPE_READ_PROFILE_ACK:
+ ret = claw_profile_event(drvdata, cmd_rep);
+ /* Stale address received, ignore and keep waiting */
+ if (ret == -EAGAIN)
+ return 0;
+ if (drvdata->waiting_for_ack &&
+ drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_PROFILE) {
+ drvdata->cmd_status = ret;
+ drvdata->waiting_for_ack = false;
+ complete(&drvdata->send_cmd_complete);
+ }
+
break;
case CLAW_COMMAND_TYPE_ACK:
- if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE)
+ if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_PROFILE ||
+ drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE)
break;
if (drvdata->orphan_ack_pending) {
@@ -493,6 +724,177 @@ static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
}
static DEVICE_ATTR_WO(reset);
+static int mkey_mapping_name_to_code(const char *name)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+ if (!strcmp(name, claw_button_mapping_key_map[i].name))
+ return claw_button_mapping_key_map[i].code;
+ }
+
+ return -EINVAL;
+}
+
+static const char *mkey_mapping_code_to_name(u8 code)
+{
+ int i;
+
+ if (code == 0xff)
+ return NULL;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+ if (claw_button_mapping_key_map[i].code == code)
+ return claw_button_mapping_key_map[i].name;
+ }
+
+ return NULL;
+}
+
+static int claw_mkey_store(struct device *dev, const char *buf, u8 mkey)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ struct claw_mkey_report report = { {0x01, cpu_to_be16(drvdata->bmap_addr[mkey])},
+ 0x07, 0x04, 0x00, {0xff, 0xff, 0xff, 0xff, 0xff} };
+ char **raw_keys __free(argv_free) = NULL;
+ int ret, key_count, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ raw_keys = argv_split(GFP_KERNEL, buf, &key_count);
+ if (!raw_keys)
+ return -ENOMEM;
+
+ if (key_count > CLAW_KEYS_MAX)
+ return -EINVAL;
+
+ if (key_count == 0)
+ goto set_buttons;
+
+ for (i = 0; i < key_count; i++) {
+ ret = mkey_mapping_name_to_code(raw_keys[i]);
+ if (ret < 0)
+ return ret;
+
+ report.codes[i] = ret;
+ }
+
+set_buttons:
+ scoped_guard(mutex, &drvdata->rom_mutex) {
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ }
+
+ return ret;
+}
+
+static int claw_mkey_show(struct device *dev, char *buf, enum claw_key_index m_key)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ struct claw_mkey_report report = { {0x01, cpu_to_be16(drvdata->bmap_addr[m_key])}, 0x07 };
+ int i, ret, count = 0;
+ const char *name;
+ u8 *codes;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes;
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING
+ : CLAW_M2_PENDING;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < CLAW_KEYS_MAX; i++) {
+ name = mkey_mapping_code_to_name(codes[i]);
+ if (name)
+ count += sysfs_emit_at(buf, count, "%s ", name);
+ }
+
+ if (!count)
+ return sysfs_emit(buf, "(not set)\n");
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+
+ ret = claw_mkey_store(dev, buf, CLAW_KEY_M1);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return claw_mkey_show(dev, buf, CLAW_KEY_M1);
+}
+static DEVICE_ATTR_RW(button_m1);
+
+static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+
+ ret = claw_mkey_store(dev, buf, CLAW_KEY_M2);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return claw_mkey_show(dev, buf, CLAW_KEY_M2);
+}
+static DEVICE_ATTR_RW(button_m2);
+
+static ssize_t button_mapping_options_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(button_mapping_options);
+
static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
int n)
{
@@ -505,10 +907,22 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu
return 0;
}
- return attr->mode;
+ /* Always show attrs available on all firmware */
+ if (attr == &dev_attr_gamepad_mode.attr ||
+ attr == &dev_attr_gamepad_mode_index.attr ||
+ attr == &dev_attr_mkeys_function.attr ||
+ attr == &dev_attr_mkeys_function_index.attr ||
+ attr == &dev_attr_reset.attr)
+ return attr->mode;
+
+ /* Hide button mapping attrs if it isn't supported */
+ return drvdata->bmap_support ? attr->mode : 0;
}
static struct attribute *claw_gamepad_attrs[] = {
+ &dev_attr_button_m1.attr,
+ &dev_attr_button_m2.attr,
+ &dev_attr_button_mapping_options.attr,
&dev_attr_gamepad_mode.attr,
&dev_attr_gamepad_mode_index.attr,
&dev_attr_mkeys_function.attr,
@@ -561,8 +975,31 @@ static void cfg_resume_fn(struct work_struct *work)
schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
}
+static void claw_features_supported(struct claw_drvdata *drvdata)
+{
+ u8 major = (drvdata->bcd_device >> 8) & 0xff;
+ u8 minor = drvdata->bcd_device & 0xff;
+
+ if (major == 0x01) {
+ drvdata->bmap_support = true;
+ if (minor >= 0x66)
+ drvdata->bmap_addr = button_mapping_addr_new;
+ else
+ drvdata->bmap_addr = button_mapping_addr_old;
+ return;
+ }
+
+ if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
+ drvdata->bmap_support = true;
+ drvdata->bmap_addr = button_mapping_addr_new;
+ return;
+ }
+}
+
static int claw_probe(struct hid_device *hdev, u8 ep)
{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_device *udev = interface_to_usbdev(intf);
struct claw_drvdata *drvdata;
int ret;
@@ -574,10 +1011,20 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
drvdata->hdev = hdev;
drvdata->ep = ep;
+ /* Determine feature level from firmware version */
+ drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice);
+ claw_features_supported(drvdata);
+
+ if (!drvdata->bmap_support)
+ dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n");
+
mutex_init(&drvdata->cfg_mutex);
+ mutex_init(&drvdata->profile_mutex);
+ mutex_init(&drvdata->rom_mutex);
spin_lock_init(&drvdata->registration_lock);
spin_lock_init(&drvdata->cmd_lock);
spin_lock_init(&drvdata->mode_lock);
+ spin_lock_init(&drvdata->profile_lock);
init_completion(&drvdata->send_cmd_complete);
INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
--
2.54.0
^ permalink raw reply related
* [PATCH v12 1/4] HID: hid-msi: Add MSI Claw configuration driver
From: Derek J. Clark @ 2026-07-14 10:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260714102640.18075-1-derekjohn.clark@gmail.com>
Adds configuration HID driver for the MSI Claw series of handheld PC's.
In this initial patch add the initial driver outline and attributes for
changing the gamepad mode, M-key behavior, and add a WO reset function.
Sending the SWITCH_MODE and RESET commands causes a USB disconnect in
the device. The completion will therefore never get hit and would trigger
an -EIO. To avoid showing the user an error for every write to these
attrs a bypass for the completion handling is introduced when timeout ==
0.
The initial version of this patch was written by Denis Benato, which
contained the initial reverse-engineering and implementation for the
gamepad mode switching. This work was later expanded by Zhouwang Huang
to include more gamepad modes. Finally, I refactored the drivers data
in/out flow and overall format to conform to kernel driver best
practices and style guides. Claude was used as an initial reviewer of
this patch.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v12:
- Fix race condition with mode events. Makes claw_hw_output_report() a
wrapper that holds cfg_mutex and adds __claw_hw_output_report(),
which has the old functionality but uses lockdep_assert_held(), and
claw_switch_mode(), which handles cfg_mutex holding and spinlock
holding for all mode functions, preventing the multiple callers issue.
- Remove goto based cleanup in __claw_hw_output_report(), adds some
duplicated code but avoids anti-pattern for cleanup.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
- Add bool to track if the driver is waiting on a sync ack with no
timeout to prevent those acks from clearing timeout acks.
v9:
- Don't use devm_device_add_group in cfg_setup_fn, do manual adding
and cleanup to prevent possible use after free.
- Use scoped_guard instead of guard in claw_remove.
- Rename gamepad_registered to gp_registered for brevity.
v8:
- Use spinlock when accessing gamepad_registered.
- Clear state machine on all errors in claw_hw_output_report.
- Wrap all branches under single cmd_lock guard in claw_raw_event.
- Reject generic ACK in claw_raw_event if waiting_cmd is for another
branch.
- Don't close hid devices that couldn't have been opened.
v7:
- Use smp_[store_release|load_acquire] pattern for checking
gamepad_registered to avoid possible races during teardown.
- Reorder reinit_completion in claw_hw_output_report to avoid race
with possible incoming ACKs.
- Reorder cancel_delayed_work_sync to ensure setup can't be re-armed
after cancel.
- Reset command state machine if hw_output_report has an error.
- Add comments to (hopefully) silence sashinko-bot warnings about the
use of endpoint matching and the impossible scenario of switching to
the alternate endpoint from userspace while the driver is bound.
- Don't use spinlock_irqsave when already in irq context.
v6:
- Add send/ack pattern to ensure synchronous acks.
- Use spinlock_irqsave instead of mutex for read/write MODE event
data.
- add select NEW_LEDS to kconfig.
- Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
config.
- Gate all attribute show/store functions with gamepad_registered,
enabling use of devm_device_add_group.
- Re-arm cfg_setup in resume if it was canceled in an early suspend.
- Don't set gamepad_mode on resume, MCU preserves state.
- Ensure all count variables are checked for > 0 characters before
setting buf - 1 to \n.
v5:
- Swap disabled & combination mkeys_function enum values.
- Ensure mode_mutex is properly init.
- Ensure claw_remove is calling hid_hw_close and not hid_hw_stop for
all paths.
v4:
- Add msi_suspend/claw_suspend.
- Reorder claw_remove to cancel all work before removing sysfs.
- Add mutex lock for removing sysfs attributes.
- Add mutex lock for MODE command data read/write.
v3:
- Ensure claw_hw_output_report is properly guarded.
- Reoder claw_probe to ensure all mutex, completion, and variable
assignments are in place prior to setting drvdata.
- Ensure gamepad_mode is set to a valid enum value in claw_probe.
v2:
- Rename driver to hid-msi from hid-msi-claw.
- Rename reusable/generic functions to msi_* from claw_*, retaining
claw specific functions.
- Add generic entrypoints for probe, remove, and raw event that route
to claw specific functions.
---
MAINTAINERS | 6 +
drivers/hid/Kconfig | 13 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 5 +
drivers/hid/hid-msi.c | 761 ++++++++++++++++++++++++++++++++++++++++++
5 files changed, 786 insertions(+)
create mode 100644 drivers/hid/hid-msi.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 6f6517bf4f97..8e2de98b768f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17965,6 +17965,12 @@ S: Odd Fixes
F: Documentation/devicetree/bindings/net/ieee802154/mrf24j40.txt
F: drivers/net/ieee802154/mrf24j40.c
+MSI HID DRIVER
+M: Derek J. Clark <derekjohn.clark@gmail.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-msi.c
+
MSI EC DRIVER
M: Nikita Kravets <teackot@gmail.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 10c12d8e6557..7766676051a5 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -492,6 +492,19 @@ config HID_GT683R
Currently the following devices are know to be supported:
- MSI GT683R
+config HID_MSI
+ tristate "MSI Claw Gamepad Support"
+ depends on USB_HID
+ select NEW_LEDS
+ select LEDS_CLASS
+ select LEDS_CLASS_MULTICOLOR
+ help
+ Support for the MSI Claw RGB and controller configuration
+
+ Say Y here to include configuration interface support for the MSI Claw Line
+ of Handheld Console Controllers. Say M here to compile this driver as a
+ module. The module will be called hid-msi.
+
config HID_KEYTOUCH
tristate "Keytouch HID devices"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 07dfdb6a49c5..80925a17b059 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -92,6 +92,7 @@ obj-$(CONFIG_HID_MAYFLASH) += hid-mf.o
obj-$(CONFIG_HID_MEGAWORLD_FF) += hid-megaworld.o
obj-$(CONFIG_HID_MICROSOFT) += hid-microsoft.o
obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o
+obj-$(CONFIG_HID_MSI) += hid-msi.o
obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o
obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o
obj-$(CONFIG_HID_NTI) += hid-nti.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 933b7943bdb5..94a9b89dc240 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1047,7 +1047,12 @@
#define USB_DEVICE_ID_MOZA_R16_R21_2 0x0010
#define USB_VENDOR_ID_MSI 0x1770
+#define USB_VENDOR_ID_MSI_2 0x0db0
#define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00
+#define USB_DEVICE_ID_MSI_CLAW_XINPUT 0x1901
+#define USB_DEVICE_ID_MSI_CLAW_DINPUT 0x1902
+#define USB_DEVICE_ID_MSI_CLAW_DESKTOP 0x1903
+#define USB_DEVICE_ID_MSI_CLAW_BIOS 0x1904
#define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400
#define USB_DEVICE_ID_N_S_HARMONY 0xc359
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
new file mode 100644
index 000000000000..8a48ce099c1c
--- /dev/null
+++ b/drivers/hid/hid-msi.c
@@ -0,0 +1,761 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for MSI Claw Handheld PC gamepads.
+ *
+ * Provides configuration support for the MSI Claw series of handheld PC
+ * gamepads. Multiple iterations of the device firmware has led to some
+ * quirks for how certain attributes are handled. The original firmware
+ * did not support remapping of the M1 (right) and M2 (left) rear paddles.
+ * Additionally, the MCU RAM address for writing configuration data has
+ * changed twice. Checks are done during probe to enumerate these variances.
+ *
+ * Copyright (c) 2026 Zhouwang Huang <honjow311@gmail.com>
+ * Copyright (c) 2026 Denis Benato <denis.benato@linux.dev>
+ * Copyright (c) 2026 Valve Corporation
+ */
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/container_of.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/kobject.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/spinlock.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define CLAW_OUTPUT_REPORT_ID 0x0f
+#define CLAW_INPUT_REPORT_ID 0x10
+
+#define CLAW_PACKET_SIZE 64
+
+#define CLAW_DINPUT_CFG_INTF_IN 0x82
+#define CLAW_XINPUT_CFG_INTF_IN 0x83
+
+enum claw_command_index {
+ CLAW_COMMAND_TYPE_NONE = 0x00,
+ CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
+ CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05,
+ CLAW_COMMAND_TYPE_ACK = 0x06,
+ CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA = 0x21,
+ CLAW_COMMAND_TYPE_SYNC_TO_ROM = 0x22,
+ CLAW_COMMAND_TYPE_SWITCH_MODE = 0x24,
+ CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE = 0x26,
+ CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK = 0x27,
+ CLAW_COMMAND_TYPE_RESET_DEVICE = 0x28,
+};
+
+enum claw_gamepad_mode_index {
+ CLAW_GAMEPAD_MODE_XINPUT = 0x01,
+ CLAW_GAMEPAD_MODE_DINPUT = 0x02,
+ CLAW_GAMEPAD_MODE_DESKTOP = 0x04,
+};
+
+static const char * const claw_gamepad_mode_text[] = {
+ [CLAW_GAMEPAD_MODE_XINPUT] = "xinput",
+ [CLAW_GAMEPAD_MODE_DINPUT] = "dinput",
+ [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop",
+};
+
+enum claw_mkeys_function_index {
+ CLAW_MKEY_FUNCTION_MACRO,
+ CLAW_MKEY_FUNCTION_DISABLED,
+ CLAW_MKEY_FUNCTION_COMBO,
+};
+
+enum claw_mode_field {
+ CLAW_FIELD_GAMEPAD_MODE,
+ CLAW_FIELD_MKEYS_FUNCTION,
+};
+
+static const char * const claw_mkeys_function_text[] = {
+ [CLAW_MKEY_FUNCTION_MACRO] = "macro",
+ [CLAW_MKEY_FUNCTION_DISABLED] = "disabled",
+ [CLAW_MKEY_FUNCTION_COMBO] = "combination",
+};
+
+struct claw_command_report {
+ u8 report_id;
+ u8 padding[2];
+ u8 header_tail;
+ u8 cmd;
+ u8 data[59];
+} __packed;
+
+struct claw_drvdata {
+ /* MCU General Variables */
+ struct completion send_cmd_complete;
+ struct delayed_work cfg_resume;
+ struct delayed_work cfg_setup;
+ spinlock_t registration_lock; /* Lock for registration read/write */
+ struct hid_device *hdev;
+ bool orphan_ack_pending;
+ struct mutex cfg_mutex; /* mutex for synchronous data */
+ bool waiting_for_ack;
+ spinlock_t cmd_lock; /* Lock for cmd data read/write */
+ u8 waiting_cmd;
+ int cmd_status;
+ u8 ep;
+
+ /* Gamepad Variables */
+ enum claw_mkeys_function_index mkeys_function;
+ enum claw_gamepad_mode_index gamepad_mode;
+ spinlock_t mode_lock; /* Lock for mode data read/write */
+ bool gp_registered;
+};
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_host_endpoint *ep;
+ struct usb_interface *intf;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ ep = intf->cur_altsetting->endpoint;
+ if (ep)
+ return ep->desc.bEndpointAddress;
+
+ return -ENODEV;
+}
+
+static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
+ struct claw_command_report *cmd_rep)
+{
+ if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) ||
+ !claw_gamepad_mode_text[cmd_rep->data[0]] ||
+ cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text))
+ return -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
+ drvdata->gamepad_mode = cmd_rep->data[0];
+ drvdata->mkeys_function = cmd_rep->data[1];
+ }
+
+ return 0;
+}
+
+static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct claw_command_report *cmd_rep;
+ int ret = 0;
+
+ if (size != CLAW_PACKET_SIZE)
+ return 0;
+
+ cmd_rep = (struct claw_command_report *)data;
+
+ if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c)
+ return 0;
+
+ dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n",
+ CLAW_PACKET_SIZE, data);
+
+ guard(spinlock_irqsave)(&drvdata->cmd_lock);
+ switch (cmd_rep->cmd) {
+ case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK:
+ ret = claw_gamepad_mode_event(drvdata, cmd_rep);
+ if (drvdata->waiting_for_ack &&
+ drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) {
+ drvdata->cmd_status = ret;
+ drvdata->waiting_for_ack = false;
+ complete(&drvdata->send_cmd_complete);
+ }
+
+ break;
+ case CLAW_COMMAND_TYPE_ACK:
+ if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE)
+ break;
+
+ if (drvdata->orphan_ack_pending) {
+ drvdata->orphan_ack_pending = false;
+ break;
+ }
+
+ if (drvdata->waiting_for_ack) {
+ drvdata->cmd_status = 0;
+ drvdata->waiting_for_ack = false;
+ complete(&drvdata->send_cmd_complete);
+ }
+ dev_dbg(&drvdata->hdev->dev, "Waiting CMD: %x\n", drvdata->waiting_cmd);
+
+ break;
+ default:
+ dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd);
+ return 0;
+ }
+
+ return ret;
+}
+
+static int msi_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN &&
+ drvdata->ep != CLAW_DINPUT_CFG_INTF_IN))
+ return 0;
+
+ return claw_raw_event(drvdata, report, data, size);
+}
+
+/* Caller must hold drvdata->cfg_mutex. */
+static int __claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
+ size_t len, unsigned int timeout)
+{
+ unsigned char *dmabuf __free(kfree) = NULL;
+ u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index };
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ size_t header_size = ARRAY_SIZE(header);
+ int ret;
+
+ lockdep_assert_held(&drvdata->cfg_mutex);
+
+ if (header_size + len > CLAW_PACKET_SIZE)
+ return -EINVAL;
+
+ /* We can't use a devm_alloc reusable buffer without side effects during suspend */
+ dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, header, header_size);
+ if (data && len)
+ memcpy(dmabuf + header_size, data, len);
+
+ reinit_completion(&drvdata->send_cmd_complete);
+ if (timeout) {
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+ drvdata->waiting_cmd = index;
+ drvdata->waiting_for_ack = true;
+ drvdata->cmd_status = -ETIMEDOUT;
+ }
+ } else {
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+ drvdata->waiting_cmd = index;
+ drvdata->waiting_for_ack = false;
+ if (index == CLAW_COMMAND_TYPE_SYNC_TO_ROM)
+ drvdata->orphan_ack_pending = true;
+ }
+ }
+
+ dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+ CLAW_PACKET_SIZE, dmabuf);
+
+ ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE);
+ if (ret < 0) {
+ if (timeout) {
+ guard(spinlock_irqsave)(&drvdata->cmd_lock);
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+ drvdata->waiting_for_ack = false;
+ }
+ return ret;
+ }
+
+ ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO;
+ if (ret) {
+ if (timeout) {
+ guard(spinlock_irqsave)(&drvdata->cmd_lock);
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+ drvdata->waiting_for_ack = false;
+ }
+ return ret;
+ }
+
+ if (timeout) {
+ ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete,
+ msecs_to_jiffies(timeout));
+
+ dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret);
+ ret = ret > 0 ? drvdata->cmd_status : ret ?: -EBUSY;
+ guard(spinlock_irqsave)(&drvdata->cmd_lock);
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+ drvdata->waiting_for_ack = false;
+ }
+ return ret;
+}
+
+static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
+ size_t len, unsigned int timeout)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ guard(mutex)(&drvdata->cfg_mutex);
+ return __claw_hw_output_report(hdev, index, data, len, timeout);
+}
+
+static int claw_switch_mode(struct hid_device *hdev, enum claw_mode_field field, u8 val)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 data[2];
+
+ guard(mutex)(&drvdata->cfg_mutex);
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
+ switch (field) {
+ case CLAW_FIELD_GAMEPAD_MODE:
+ data[0] = val;
+ data[1] = drvdata->mkeys_function;
+ break;
+ case CLAW_FIELD_MKEYS_FUNCTION:
+ data[0] = drvdata->gamepad_mode;
+ data[1] = val;
+ break;
+ }
+ }
+
+ return __claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data,
+ ARRAY_SIZE(data), 0);
+}
+
+static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int i, ret = -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+ if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) {
+ ret = i;
+ break;
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ ret = claw_switch_mode(hdev, CLAW_FIELD_GAMEPAD_MODE, ret);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+ i = drvdata->gamepad_mode;
+
+ if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]);
+}
+static DEVICE_ATTR_RW(gamepad_mode);
+
+static ssize_t gamepad_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+ if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+ continue;
+ count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]);
+ }
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(gamepad_mode_index);
+
+static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int i, ret = -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) {
+ if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) {
+ ret = i;
+ break;
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ ret = claw_switch_mode(hdev, CLAW_FIELD_MKEYS_FUNCTION, ret);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+ i = drvdata->mkeys_function;
+
+ if (i >= ARRAY_SIZE(claw_mkeys_function_text))
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]);
+}
+static DEVICE_ATTR_RW(mkeys_function);
+
+static ssize_t mkeys_function_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(mkeys_function_index);
+
+static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ bool val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+
+ if (!val)
+ return -EINVAL;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+static DEVICE_ATTR_WO(reset);
+
+static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
+ int n)
+{
+ struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj));
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata) {
+ dev_warn(&hdev->dev,
+ "Failed to get drvdata from kobj. Gamepad attributes are not available.\n");
+ return 0;
+ }
+
+ return attr->mode;
+}
+
+static struct attribute *claw_gamepad_attrs[] = {
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_index.attr,
+ &dev_attr_mkeys_function.attr,
+ &dev_attr_mkeys_function_index.attr,
+ &dev_attr_reset.attr,
+ NULL,
+};
+
+static const struct attribute_group claw_gamepad_attr_group = {
+ .attrs = claw_gamepad_attrs,
+ .is_visible = claw_gamepad_attr_is_visible,
+};
+
+static void cfg_setup_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup);
+ int ret;
+
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE,
+ NULL, 0, 25);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to setup device, can't read gamepad mode: %d\n", ret);
+ return;
+ }
+
+ /* Add sysfs attributes after we get the device state */
+ ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to setup device, can't create gamepad attrs: %d\n", ret);
+ return;
+ }
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock)
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->gp_registered, true);
+
+ kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+}
+
+static void cfg_resume_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume);
+
+ guard(spinlock_irqsave)(&drvdata->registration_lock);
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
+}
+
+static int claw_probe(struct hid_device *hdev, u8 ep)
+{
+ struct claw_drvdata *drvdata;
+ int ret;
+
+ drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
+ if (!drvdata)
+ return -ENOMEM;
+
+ drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
+ drvdata->hdev = hdev;
+ drvdata->ep = ep;
+
+ mutex_init(&drvdata->cfg_mutex);
+ spin_lock_init(&drvdata->registration_lock);
+ spin_lock_init(&drvdata->cmd_lock);
+ spin_lock_init(&drvdata->mode_lock);
+ init_completion(&drvdata->send_cmd_complete);
+ INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
+ INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
+
+ /* For control interface: open the HID transport for sending commands. */
+ ret = hid_hw_open(hdev);
+ if (ret)
+ return ret;
+
+ hid_set_drvdata(hdev, drvdata);
+ schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
+
+ return 0;
+}
+
+static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ int ret;
+ u8 ep;
+
+ if (!hid_is_usb(hdev)) {
+ ret = -ENODEV;
+ goto err_probe;
+ }
+
+ ret = hid_parse(hdev);
+ if (ret)
+ goto err_probe;
+
+ /* Set quirk to create separate input devices per HID application */
+ hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ goto err_probe;
+
+ /* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */
+ ret = get_endpoint_address(hdev);
+ if (ret < 0)
+ goto err_stop_hw;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) {
+ ret = claw_probe(hdev, ep);
+ if (ret)
+ goto err_stop_hw;
+ }
+
+ return 0;
+
+err_stop_hw:
+ hid_hw_stop(hdev);
+err_probe:
+ return dev_err_probe(&hdev->dev, ret, "Failed to init device\n");
+}
+
+static void claw_remove(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ bool gp_registered;
+
+ if (!drvdata)
+ return;
+
+ cancel_delayed_work_sync(&drvdata->cfg_resume);
+ cancel_delayed_work_sync(&drvdata->cfg_setup);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ gp_registered = smp_load_acquire(&drvdata->gp_registered);
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->gp_registered, false);
+ }
+
+ if (gp_registered)
+ device_remove_group(&hdev->dev, &claw_gamepad_attr_group);
+
+ hid_hw_close(hdev);
+}
+
+static void msi_remove(struct hid_device *hdev)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ goto hw_stop;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ claw_remove(hdev);
+
+hw_stop:
+ hid_hw_stop(hdev);
+}
+
+static int claw_resume(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata)
+ return -ENODEV;
+
+ /* MCU can take up to 500ms to be ready after resume */
+ schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500));
+ return 0;
+}
+
+static int msi_resume(struct hid_device *hdev)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ return 0;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ return claw_resume(hdev);
+
+ return 0;
+}
+
+static int claw_suspend(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata)
+ return -ENODEV;
+
+ cancel_delayed_work_sync(&drvdata->cfg_resume);
+ cancel_delayed_work_sync(&drvdata->cfg_setup);
+
+ return 0;
+}
+
+static int msi_suspend(struct hid_device *hdev, pm_message_t msg)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ return 0;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ return claw_suspend(hdev);
+
+ return 0;
+}
+
+static const struct hid_device_id msi_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, msi_devices);
+
+static struct hid_driver msi_driver = {
+ .name = "hid-msi",
+ .id_table = msi_devices,
+ .raw_event = msi_raw_event,
+ .probe = msi_probe,
+ .remove = msi_remove,
+ .resume = msi_resume,
+ .suspend = pm_ptr(msi_suspend),
+};
+module_hid_driver(msi_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_AUTHOR("Zhouwang Huang <honjow311@gmail.com>");
+MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads");
--
2.54.0
^ permalink raw reply related
* [PATCH v12 0/4] Add MSI Claw HID Configuration Driver
From: Derek J. Clark @ 2026-07-14 10:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
This series adds an HID Configuration driver for the MSI Claw line of
Handheld Gaming PC's. The MSI Claw HID interface provides multiple
features, such as the ability to switch between xinput, dinput, and a
desktop mode, RGB control, rumble intensity, and mapping of the rear "M"
keys. There are additional gamepad modes that are not included in this
driver as they appear to be used in assembly line testing or are
incomplete in the firmware. During my testing I found them to be unstable.
The initial version of this driver was written by Denis Benato, which
contained the initial reverse-engineering and implementation for the
gamepad mode switching. This work was later expanded by Zhouwang Huang
to include more gamepad modes and additional features. Finally, I
refactored the entire driver, fixed multiple bugs, and refined the overall
format to conform to kernel driver best practices and style guide.
Claude was used initially by Zhouwang Huang to quickly parse HID captures
during the reverse-engineering of some of the features. Since Claude had
already been used, as a test of its capabilities I had it implement the
rumble intensity attribute after I had already rewritten most of the
driver, which I then manually edited to fix some mistakes. I also used
Claude to review the driver and these patches for any mistakes and bugs.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v12:
- Fix race condition with mode events. Makes claw_hw_output_report() a
wrapper that holds cfg_mutex and adds __claw_hw_output_report(),
which has the old functionality but uses lockdep_assert_held(), and
claw_switch_mode(), which handles cfg_mutex holding and spinlock
holding for all mode functions, preventing the multiple callers issue.
- Remove goto based cleanup in __claw_hw_output_report(), adds some
duplicated code but avoids anti-pattern for cleanup.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
- Add bool to track if the driver is waiting on a sync ack with no
timeout to prevent those acks from clearing timeout acks.
- On address mismatch, assume stale message return and keep waiting for
correct message.
- On remove, cancel the rgb_queue last to avoid a re-arming, and in
rgb_queue_fn check rgb_registered to avoid running after the sysfs
group has been removed.
- Add note to led_cdev.name to (hopefully) silence sashiko-bot about
an impossible double device collision.
- Explicitly check if the current address is less than the rgb_address
to avoid invalid overflow math.
v11: https://lore.kernel.org/linux-input/20260529072111.7565-1-derekjohn.clark@gmail.com/#r
- Restore dropped changes from v10.
v10: https://lore.kernel.org/linux-input/20260527222122.10620-1-derekjohn.clark@gmail.com/
- Remove additional gamepad_registered variable left over after rename
to gp_registered.
v9: https://lore.kernel.org/linux-input/20260525020543.519082-1-derekjohn.clark@gmail.com/
- Don't use devm_* functions in cfg_setup_fn, do manual adding
and cleanup to prevent possible use after free.
- Use scoped_guard instead of guard in claw_remove.
- Rename gamepad_registered to gp_registered for brevity.
- Check for drvdata in rgb_queue_fn to avoid use after free during
teardown.
- Ensure rgb_queue work is canceled during suspend.
- Limit guard usage in cfg_setup_fn to avoid holding a lock during
registration and add group events.
v8: https://lore.kernel.org/linux-input/20260522015518.1111290-1-derekjohn.clark@gmail.com/
- Use spinlock when accessing gamepad_registered.
- Clear state machine on all errors in claw_hw_output_report.
- Wrap all branches under single cmd_lock guard in claw_raw_event.
- Reject generic ACK in claw_raw_event if waiting_cmd is for another
branch.
- Wrap all branches under single cmd_lock guard in claw_raw_event.
- Reject generic ACK in claw_raw_event if waiting_cmd is for another
branch.
- Don't close hid devices that couldn't have been opened.
- Ensure led_classdev is unregistered if adding attribute group fails.
- Reorder remove actions to ensure no use-after free or rearming cleared
flags.
v7: https://lore.kernel.org/linux-input/20260520013158.3633277-1-derekjohn.clark@gmail.com/
- Use smp_[store_release|load_acquire] pattern for checking
gamepad_registered and rgb_registered to avoid possible races during
teardown.
- Reorder reinit_completion in claw_hw_output_report to avoid race
with possible incoming ACKs.
- Reorder cancel_delayed_work_sync to ensure setup can't be re-armed
after cancel.
- Reset command state machine if hw_output_report has an error.
- Add comments to (hopefully) silence sashinko-bot warnings about the
use of endpoint matching and the impossible scenario of switching to
the alternate endpoint from userspace while the driver is bound.
- Don't use spinlock_irqsave when already in irq context.
- Add profile_lock for read/write profile_pending.
- Use struct for mkey reports and rumble reports, following the
pattern established by rgb reports previously.
- Add gating to cfg_setup_fn, allowing either gamepad settings or rgb
settings to populate if the other fails for any reason.
- Match on write address for rumble and mkey reports to prevent late
ACK from causing synchronization errors.
v6: https://lore.kernel.org/linux-input/20260518222935.1802071-1-derekjohn.clark@gmail.com/
- Add send/ack pattern to ensure synchronous acks.
- Use spinlock_irqsave instead of mutex for read/write MODE event
data.
- add select NEW_LEDS to kconfig.
- Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz
config.
- Gate all attribute show/store functions with gamepad_registered or
rgb_registered, enabling use of devm_device_add_group and ending
the need to hold a mutex during remove.
- Don't set gamepad_mode on resume, MCU preserves state.
- Ensure all count variables are checked for > 0 characters before
setting buf - 1 to \n.
- Re-arm cfg_setup in resume if it was canceled in an early suspend.
- Remove duplicated argv_free macro.
- Add spinlock_irqsave vice mutex for read/write access on attribute
variables.
v5: https://lore.kernel.org/linux-input/20260517013925.3120314-1-derekjohn.clark@gmail.com/
- Swap disabled & combination mkeys_function enum values.
- Fix bug introduced in v5 where claw_buttons_store would return
-EINVAL on all valid key entries.
- Ensure mode_mutex is properly init.
- Ensure claw_remove is calling hid_hw_close and not hid_hw_stop for
all paths.
- Ensure adding "DISABLED" key to valid entries is done in the correct
patch.
- Re-enable sending an empty string to clear button mappings in
addition to setting DISABLED.
- Move adding the RGB device into cfg_setup to prevent led core
attributes from being written to prior to setup completing.
- Ensure frame_lock is properly init.
- Change variable names in RGB functions from frame and zone to f and
z respectively to fit all scoped_guard actions in 100 columns.
v4: https://lore.kernel.org/linux-input/20260516042841.500299-1-derekjohn.clark@gmail.com/
- Add msi_suspend/claw_suspend.
- Reorder claw_remove to cancel all work before removing sysfs.
- Add mutex lock for removing sysfs attributes.
- Add mutex lock for MODE command data read/write.
- Change dev_warn to dev_dbg in claw_profile_event.
- use __free with DEFINE_FREE macro for argv instead of manually
running argv_free, cleaining up scoped_guard goto.
- Fix frame_calc validity check to use >=.
- Use spinlock instead of mutex in raw_event and related attribute
_store function.
- Ensure delayed work is canceled in suspend & canceled before sysfs
attribute removal.
v3: https://lore.kernel.org/linux-input/20260515033622.2095277-1-derekjohn.clark@gmail.com/
- Add mutex for read/write if rgb frame data.
- Ensure claw_hw_output_report is properly guarded.
- Remove setting rgb_frame_count when reading rgb profiles as it always
returns garbage data.
- Ensure rgb_speed is getting drvdata from a valid lookup (not hdev).
- Use scoped_guard where necessary.
- Reoder claw_probe to ensure all mutex, completion, and variable
assignments are in place prior to setting drvdata.
- Ensure gamepad_mode is set to a valid enum value in claw_probe.
v2: https://lore.kernel.org/linux-input/20260513231445.3213501-1-derekjohn.clark@gmail.com/
- Use mutexes to guard SYNC_TO_ROM calls and pending_profile calls.
- Rename driver to hid-msi and add generic entrypoints for
probe/resume/remove that call claw specific functions in order to
future proof the driver for other MSI HID interfaces.
- Fix various bugs and formatting issues.
v1: https://lore.kernel.org/linux-input/20260510043510.442807-1-derekjohn.clark@gmail.com/
Derek J. Clark (4):
HID: hid-msi: Add MSI Claw configuration driver
HID: hid-msi: Add M-key mapping attributes
HID: hid-msi: Add RGB control interface
HID: hid-msi: Add Rumble Intensity Attributes
MAINTAINERS | 6 +
drivers/hid/Kconfig | 13 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 5 +
drivers/hid/hid-msi.c | 2058 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 2083 insertions(+)
create mode 100644 drivers/hid/hid-msi.c
--
2.54.0
^ permalink raw reply
* Re: What's cooking in zh_CN (Jul 2026)
From: Alex Shi @ 2026-07-14 10:15 UTC (permalink / raw)
To: Dongliang Mu, Weijie Yuan, Dongliang Mu
Cc: linux-doc, Alex Shi, Yanteng Si, Ben Guo, Gary Guo, Yan Zhu,
Doehyun Baek, Jiandong Qiu
In-Reply-To: <cc922c1b-49a5-4c3a-855c-af8eb6539f62@hust.edu.cn>
On 2026/7/14 17:54, Dongliang Mu wrote:
>>>> Since I made many noise these days on the list, which took up a lot of
>>>> maintainers' time. This email summarizes the patches for zh_CN that are
>>>> currently pending on the mailing list.
>>> This is awesome. Maybe we can establish a dashboard for the activities
>>> in zh_CN/TW related patches of linux-doc.
>>>
>>> I personally kept a knowledge base in the IMA (an app for storing
>>> knowledge base in the cloud) in our club to monitor these activities.
>> I remember kernel.org itself already provides this:
>>
>> https://patchwork.kernel.org/
>>
>> Is this similar to your idea? But I haven't look deep into patchwork
>> before.
>>
>> Of source, having our own thing is definitely okay.
>
>
> I am not familar with patchwork too. How about Alex?
I don't know if the patchwork has a similar feature like this email,
does it?
Thanks!
^ permalink raw reply
* Re: [PATCH v4 0/4] Introduce Per-CPU Work helpers (was QPW)
From: Sebastian Andrzej Siewior @ 2026-07-14 10:12 UTC (permalink / raw)
To: Leonardo Bras
Cc: Jonathan Corbet, Shuah Khan, Peter Zijlstra, Ingo Molnar,
Will Deacon, Boqun Feng, Waiman Long, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Jann Horn, Pedro Falcato, Brendan Jackman, Johannes Weiner,
Zi Yan, Harry Yoo, Hao Li, Christoph Lameter, David Rientjes,
Roman Gushchin, Chris Li, Kairui Song, Kemeng Shi, Nhat Pham,
Baoquan He, Barry Song, Youngjun Park, Qi Zheng, Shakeel Butt,
Axel Rasmussen, Yuanchu Xie, Wei Xu, Borislav Petkov (AMD),
Randy Dunlap, Thomas Gleixner, Feng Tang, Dapeng Mi, Kees Cook,
Marco Elver, Jakub Kicinski, Li RongQing, Eric Biggers,
Paul E. McKenney, Nathan Chancellor, Miguel Ojeda, Nicolas Schier,
Thomas Weißschuh, Douglas Anderson, Gary Guo,
Christian Brauner, Pasha Tatashin, Masahiro Yamada, Coiby Xu,
Frederic Weisbecker, linux-doc, linux-kernel, linux-mm,
linux-rt-devel
In-Reply-To: <alVV44RO_SolaqMW@WindFlash>
On 2026-07-13 18:17:23 [-0300], Leonardo Bras wrote:
> IIRC there are multiple users of this mechanism around the kernel. I
> remember picking the first three (swap, slub and memcontrol) as examples of
> how to use pwlocks (QPW at the time). With the concept proven, I could then
> proceed to work with other potential users to replace it. (We ended up
> dropping memcontrol in the process)
>
> The main idea is to replace as many potential users as possible to reduce
> as much as possible the amount of IPIs in isolated cpus, and allow Linux
> to run workloads which require much lower latency.
Right. I am not entirely opposed to this idea. If you can't improve the
situation by other means I would consider this as the last resort. If
the mm folks are on board with this, then even better.
> This idea came as a general solution to a bunch of latency violations
> Marcelo and I were coming across.
>
> Thanks!
> Leo
Sebastian
^ permalink raw reply
* Re: What's cooking in zh_CN (Jul 2026)
From: Dongliang Mu @ 2026-07-14 9:54 UTC (permalink / raw)
To: Weijie Yuan, Dongliang Mu
Cc: linux-doc, Alex Shi, Yanteng Si, Ben Guo, Gary Guo, Yan Zhu,
Doehyun Baek, Jiandong Qiu
In-Reply-To: <alYGtkVxW_0N-VqE@wyuan.org>
On 7/14/26 5:51 PM, Weijie Yuan wrote:
> On Tue, Jul 14, 2026 at 05:32:12PM +0800, Dongliang Mu wrote:
>> On Tue, Jul 14, 2026 at 12:51 AM Weijie Yuan <wy@wyuan.org> wrote:
>>> Hi all,
>>>
>>> Since I made many noise these days on the list, which took up a lot of
>>> maintainers' time. This email summarizes the patches for zh_CN that are
>>> currently pending on the mailing list.
>> This is awesome. Maybe we can establish a dashboard for the activities
>> in zh_CN/TW related patches of linux-doc.
>>
>> I personally kept a knowledge base in the IMA (an app for storing
>> knowledge base in the cloud) in our club to monitor these activities.
> I remember kernel.org itself already provides this:
>
> https://patchwork.kernel.org/
>
> Is this similar to your idea? But I haven't look deep into patchwork
> before.
>
> Of source, having our own thing is definitely okay.
I am not familar with patchwork too. How about Alex?
Dongliang Mu
>>> * Doehyun Baek (2026-07-08) 7 commits
>>> [PATCH v6 0/7] docs/zh_CN: update DAMON translations
>>> [PATCH v6 1/7] docs/zh_CN: update DAMON design translation
>>> [PATCH v6 2/7] docs/zh_CN: add DAMON_STAT usage translation
>>> [PATCH v6 3/7] docs/zh_CN: update DAMON index translation
>>> [PATCH v6 4/7] docs/zh_CN: update DAMON start translation
>>> [PATCH v6 5/7] docs/zh_CN: update DAMON usage translation
>>> [PATCH v6 6/7] docs/zh_CN: update DAMON reclaim translation
>>> [PATCH v6 7/7] docs/zh_CN: update DAMON LRU sort translation
>> Maybe I can take a review tomorrow.
> No rush of course ;-)
>
>>> Okay, I have checked the 200 most recent messages on the mailing list,
>>> going back to 15:09 UTC on February 25, 2026. If I have missed anything,
>>> or I made a mistake somewhere, please let me know.
>>>
>>> As you may have noticed, I borrowed (stole) this idea from Junio C
>>> Hamano. Sending this kind of message, somewhat like a weekly status
>>> report, not only helps maintainers keep track of outstanding work, but
>>> also lets contributors know the current status of their patches.
>> As mentioned before, can we have a dashboard to see the status of
>> patches in zh_CN/TW? This is more useful in my mind.
>>
>>> More importantly, it gives newcomers an overview of the current state of
>>> the project. New contributors can begin not only by submitting patches,
>>> but also by reviewing patches already posted to the mailing list,
>>> thereby learning how our workflow operates. This may also help reduce
>>> the review burden on our friendly maintainers.
>>>
>>> I would like to try this kind of periodic report as an experimental
>>> effort, with its frequency adjusted according to the size of the patch
>>> backlog and the level of activity on the mailing list. What do you
>>> think? Please feel free to make comments.
>> I think this is fine since linux-doc or narrowly zh_CN/TW do not have
>> many volumes of patches per day.
> Exactly. I think Junio C Hamano has a script to do this automaticly, but
> I did this manually because our volumes is not that high as Git.
>
>>> By the way, I borrowed the subject line directly from Git's "What's
>>> cooking in git.git". Does anyone have a better suggestion for the name? ;-)
>> fine with this title
> OK, let's keep it now.
^ permalink raw reply
* Re: What's cooking in zh_CN (Jul 2026)
From: Weijie Yuan @ 2026-07-14 9:51 UTC (permalink / raw)
To: Dongliang Mu
Cc: linux-doc, Alex Shi, Yanteng Si, Dongliang Mu, Ben Guo, Gary Guo,
Yan Zhu, Doehyun Baek, Jiandong Qiu
In-Reply-To: <CAD-N9QVXqYxtsn7YuUtCDWrwwk5+iFAkT2jcs26zbDUfwhAwsQ@mail.gmail.com>
On Tue, Jul 14, 2026 at 05:32:12PM +0800, Dongliang Mu wrote:
> On Tue, Jul 14, 2026 at 12:51 AM Weijie Yuan <wy@wyuan.org> wrote:
> >
> > Hi all,
> >
> > Since I made many noise these days on the list, which took up a lot of
> > maintainers' time. This email summarizes the patches for zh_CN that are
> > currently pending on the mailing list.
>
> This is awesome. Maybe we can establish a dashboard for the activities
> in zh_CN/TW related patches of linux-doc.
>
> I personally kept a knowledge base in the IMA (an app for storing
> knowledge base in the cloud) in our club to monitor these activities.
I remember kernel.org itself already provides this:
https://patchwork.kernel.org/
Is this similar to your idea? But I haven't look deep into patchwork
before.
Of source, having our own thing is definitely okay.
> > * Doehyun Baek (2026-07-08) 7 commits
> > [PATCH v6 0/7] docs/zh_CN: update DAMON translations
> > [PATCH v6 1/7] docs/zh_CN: update DAMON design translation
> > [PATCH v6 2/7] docs/zh_CN: add DAMON_STAT usage translation
> > [PATCH v6 3/7] docs/zh_CN: update DAMON index translation
> > [PATCH v6 4/7] docs/zh_CN: update DAMON start translation
> > [PATCH v6 5/7] docs/zh_CN: update DAMON usage translation
> > [PATCH v6 6/7] docs/zh_CN: update DAMON reclaim translation
> > [PATCH v6 7/7] docs/zh_CN: update DAMON LRU sort translation
>
> Maybe I can take a review tomorrow.
No rush of course ;-)
> > Okay, I have checked the 200 most recent messages on the mailing list,
> > going back to 15:09 UTC on February 25, 2026. If I have missed anything,
> > or I made a mistake somewhere, please let me know.
> >
> > As you may have noticed, I borrowed (stole) this idea from Junio C
> > Hamano. Sending this kind of message, somewhat like a weekly status
> > report, not only helps maintainers keep track of outstanding work, but
> > also lets contributors know the current status of their patches.
>
> As mentioned before, can we have a dashboard to see the status of
> patches in zh_CN/TW? This is more useful in my mind.
>
> >
> > More importantly, it gives newcomers an overview of the current state of
> > the project. New contributors can begin not only by submitting patches,
> > but also by reviewing patches already posted to the mailing list,
> > thereby learning how our workflow operates. This may also help reduce
> > the review burden on our friendly maintainers.
> >
> > I would like to try this kind of periodic report as an experimental
> > effort, with its frequency adjusted according to the size of the patch
> > backlog and the level of activity on the mailing list. What do you
> > think? Please feel free to make comments.
>
> I think this is fine since linux-doc or narrowly zh_CN/TW do not have
> many volumes of patches per day.
Exactly. I think Junio C Hamano has a script to do this automaticly, but
I did this manually because our volumes is not that high as Git.
> > By the way, I borrowed the subject line directly from Git's "What's
> > cooking in git.git". Does anyone have a better suggestion for the name? ;-)
>
> fine with this title
OK, let's keep it now.
^ permalink raw reply
* Re: [PATCH v21 06/14] dmaengine: qcom: bam_dma: add support for BAM locking
From: Stephan Gerhold @ 2026-07-14 9:49 UTC (permalink / raw)
To: Bartosz Golaszewski
Cc: Vinod Koul, Jonathan Corbet, Thara Gopinath, Herbert Xu,
David S. Miller, Udit Tiwari, Md Sadre Alam, Dmitry Baryshkov,
Manivannan Sadhasivam, Bjorn Andersson, Peter Ujfalusi,
Michal Simek, Frank Li, Andy Gross, Neil Armstrong, dmaengine,
linux-doc, linux-kernel, linux-arm-msm, linux-crypto,
linux-arm-kernel, brgl, Bartosz Golaszewski
In-Reply-To: <20260713-qcom-qce-cmd-descr-v21-6-bc2583e18475@oss.qualcomm.com>
On Mon, Jul 13, 2026 at 03:01:07PM +0200, Bartosz Golaszewski wrote:
> Add support for BAM pipe locking. To that end: when starting DMA on an RX
> channel - prepend the existing queue of issued descriptors with an
> additional "dummy" command descriptor with the LOCK bit set. Once the
> transaction is done (no more issued descriptors), issue one more dummy
> descriptor with the UNLOCK bit.
>
> We *must* wait until the transaction is signalled as done because we
> must not perform any writes into config registers while the engine is
> busy.
>
> The dummy writes must be issued into a scratchpad register of the client
> so provide a mechanism to communicate the right address via descriptor
> metadata.
>
> Reviewed-by: Manivannan Sadhasivam <mani@kernel.org>
> Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
Thanks for the fixes. The lock/unlock sequence looks good to me now,
I commented on a couple of minor things below that would be good to fix
(some of them are also reported by Sashiko).
> ---
> drivers/dma/qcom/bam_dma.c | 191 +++++++++++++++++++++++++++++++++++++--
> include/linux/dma/qcom_bam_dma.h | 14 +++
> 2 files changed, 198 insertions(+), 7 deletions(-)
>
> diff --git a/drivers/dma/qcom/bam_dma.c b/drivers/dma/qcom/bam_dma.c
> index f3e713a5259c2c7c24cfdcec094814eb1202971a..f08549ee3872eece85884606d6ee9e540ee688ca 100644
> --- a/drivers/dma/qcom/bam_dma.c
> +++ b/drivers/dma/qcom/bam_dma.c
> [...]
> @@ -686,6 +702,35 @@ static int bam_slave_config(struct dma_chan *chan,
> return 0;
> }
>
> +static int bam_metadata_attach(struct dma_async_tx_descriptor *desc, void *data, size_t len)
> +{
> + struct bam_chan *bchan = to_bam_chan(desc->chan);
> + const struct bam_device_data *bdata = bchan->bdev->dev_data;
> + struct bam_desc_metadata *metadata = data;
> +
> + if (!data)
Doesn't really matter much, but since the parameter exists you might as
well add
&& len == sizeof(*metadata)
here to be sure.
> + return -EINVAL;
> +
> + if (!bdata->pipe_lock_supported)
> + /*
> + * The client wants to use locking but this BAM version doesn't
> + * support it. Don't return an error here as this will stop the
> + * client from using DMA at all for no reason.
> + */
> + return 0;
> +
> + guard(spinlock_irqsave)(&bchan->vc.lock);
> +
> + bchan->scratchpad_addr = metadata->scratchpad_addr;
> + bchan->direction = metadata->direction;
> +
> + return 0;
> +}
> +
> +static const struct dma_descriptor_metadata_ops bam_metadata_ops = {
> + .attach = bam_metadata_attach,
> +};
I'm not sure if we have discussed this before, but could we avoid
re-programming the scratchpad_addr all the time by placing it into
struct dma_slave_config -> peripheral_config? It still feels awkward to
me to place a global constant configuration value into per-descriptor
metadata.
> +
> /**
> * bam_prep_slave_sg - Prep slave sg transaction
> *
> [...]
> @@ -802,6 +851,7 @@ static int bam_dma_terminate_all(struct dma_chan *chan)
> }
>
> vchan_get_all_descriptors(&bchan->vc, &head);
> + bchan->bam_locked = false;
I wonder about the implications of this. If the LOCK descriptor was
already processed, will we cause a deadlock if we never submit the
UNLOCK descriptor? Or I guess bam_reset_channel() might reset the lock
as well?
> }
>
> vchan_dma_desc_free_list(&bchan->vc, &head);
> [...]
> @@ -870,6 +929,7 @@ static u32 process_channel_irqs(struct bam_device *bdev)
> {
> u32 i, srcs, pipe_stts, offset, avail;
> struct bam_async_desc *async_desc, *tmp;
> + struct bam_desc_hw *hdesc;
>
> srcs = readl_relaxed(bam_addr(bdev, 0, BAM_IRQ_SRCS_EE));
>
> @@ -919,13 +979,20 @@ static u32 process_channel_irqs(struct bam_device *bdev)
> * push back to front of desc_issued so that
> * it gets restarted by the work queue.
> */
> +
> + list_del(&async_desc->desc_node);
> if (!async_desc->num_desc) {
> - vchan_cookie_complete(&async_desc->vd);
> + hdesc = async_desc->desc;
> + u16 flags = le16_to_cpu(hdesc->flags);
Is this unused? Also a bit odd to have hdesc declared outside of the
loop and flags declared inside.
> +
> + if (async_desc->is_lock_desc)
> + bam_dma_free_lock_desc(&async_desc->vd);
> + else
> + vchan_cookie_complete(&async_desc->vd);
> } else {
> list_add(&async_desc->vd.node,
> &bchan->vc.desc_issued);
> }
> - list_del(&async_desc->desc_node);
> }
> }
>
> @@ -1046,13 +1113,102 @@ static void bam_apply_new_config(struct bam_chan *bchan,
> bchan->reconfigure = 0;
> }
>
> +static struct bam_async_desc *
> +bam_make_lock_desc(struct bam_chan *bchan, unsigned long flag)
> +{
> + struct dma_chan *chan = &bchan->vc.chan;
> + struct bam_async_desc *async_desc;
> + struct bam_desc_hw *desc;
> + struct virt_dma_desc *vd;
> + struct virt_dma_chan *vc;
> + unsigned int mapped;
> +
> + async_desc = kzalloc_flex(*async_desc, desc, 1, GFP_NOWAIT);
> + if (!async_desc) {
> + dev_err(bchan->bdev->dev, "failed to allocate the BAM lock descriptor\n");
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + sg_init_table(&async_desc->lock_sg, 1);
> +
> + async_desc->num_desc = 1;
> + async_desc->curr_desc = async_desc->desc;
> + async_desc->dir = DMA_MEM_TO_DEV;
> + async_desc->is_lock_desc = true;
> +
> + desc = async_desc->desc;
> +
> + bam_prep_ce_le32(&async_desc->lock_ce, bchan->scratchpad_addr, BAM_WRITE_COMMAND, 0);
> + sg_set_buf(&async_desc->lock_sg, &async_desc->lock_ce, sizeof(async_desc->lock_ce));
> +
> + mapped = dma_map_sg(chan->slave, &async_desc->lock_sg, 1, DMA_TO_DEVICE);
I agree with Sashiko that mapping using the BAM device
(bchan->bdev->dev) would be more precise, since the BAM will be reading
the descriptor. (It doesn't matter in practice since both BAM and
consumer usually have the same IOMMUs defined.)
> + if (!mapped) {
> + kfree(async_desc);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + desc->flags |= cpu_to_le16(DESC_FLAG_CMD | flag);
> + desc->addr = sg_dma_address(&async_desc->lock_sg);
cpu_to_le32()
> + desc->size = cpu_to_le16(sizeof(struct bam_cmd_element));
> +
> + vc = &bchan->vc;
> + vd = &async_desc->vd;
> +
> + dma_async_tx_descriptor_init(&vd->tx, &vc->chan);
> + vd->tx.flags = DMA_PREP_CMD;
> + vd->tx_result.result = DMA_TRANS_NOERROR;
> + vd->tx_result.residue = 0;
> +
> + return async_desc;
> +}
> +
> [...]
> @@ -1072,6 +1229,18 @@ static void bam_start_dma(struct bam_chan *bchan)
> return;
>
> while (vd && !IS_BUSY(bchan)) {
> + /*
> + * Open a LOCK/UNLOCK bracket around each fresh sequence.
> + * Sentinels inserted by bam_setup_pipe_lock() are skipped: they
> + * already have bam_locked set and must not trigger a second pair.
> + */
> + if (!bchan->bam_locked &&
> + !container_of(vd, struct bam_async_desc, vd)->is_lock_desc) {
Do we need the ->is_lock_desc check here? Looks redundant to me.
> + ret = bam_setup_pipe_lock(bchan);
> + if (ret == 0 && bchan->bam_locked)
> + vd = vchan_next_desc(&bchan->vc);
Do we want some error handling here? If there is an error, this will
silently continue queuing everything without any locking.
Thanks,
Stephan
^ permalink raw reply
* Re: [PATCH v4 4/4] slub: apply new pw_queue_on() interface
From: Sebastian Andrzej Siewior @ 2026-07-14 9:47 UTC (permalink / raw)
To: Leonardo Bras
Cc: Jonathan Corbet, Shuah Khan, Peter Zijlstra, Ingo Molnar,
Will Deacon, Boqun Feng, Waiman Long, Andrew Morton,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Jann Horn, Pedro Falcato, Brendan Jackman, Johannes Weiner,
Zi Yan, Harry Yoo, Hao Li, Christoph Lameter, David Rientjes,
Roman Gushchin, Chris Li, Kairui Song, Kemeng Shi, Nhat Pham,
Baoquan He, Barry Song, Youngjun Park, Qi Zheng, Shakeel Butt,
Axel Rasmussen, Yuanchu Xie, Wei Xu, Borislav Petkov (AMD),
Randy Dunlap, Feng Tang, Dapeng Mi, Kees Cook, Marco Elver,
Jakub Kicinski, Li RongQing, Eric Biggers, Paul E. McKenney,
Nathan Chancellor, Nicolas Schier, Miguel Ojeda,
Thomas Weißschuh, Thomas Gleixner, Douglas Anderson,
Gary Guo, Christian Brauner, Pasha Tatashin, Coiby Xu,
Masahiro Yamada, Frederic Weisbecker, linux-doc, linux-kernel,
linux-mm, linux-rt-devel, Marcelo Tosatti
In-Reply-To: <alVbaf-UVMNaEtvk@WindFlash>
On 2026-07-13 18:40:57 [-0300], Leonardo Bras wrote:
> On Mon, Jul 13, 2026 at 09:36:34AM +0200, Sebastian Andrzej Siewior wrote:
> > On 2026-07-12 19:35:28 [-0300], Leonardo Bras wrote:
> > > On Wed, May 20, 2026 at 04:53:08PM +0200, Sebastian Andrzej Siewior wrote:
> > > > On 2026-05-18 22:27:50 [-0300], Leonardo Bras wrote:
> > > > > @@ -4733,121 +4735,121 @@ void *alloc_from_pcs(struct kmem_cache *s, gfp_t gfp, int node)
> > > > >
> > > > > /*
> > > > > * We assume the percpu sheaves contain only local objects although it's
> > > > > * not completely guaranteed, so we verify later.
> > > > > */
> > > > > if (unlikely(node_requested && node != numa_mem_id())) {
> > > > > stat(s, ALLOC_NODE_MISMATCH);
> > > > > return NULL;
> > > > > }
> > > > >
> > > > > - if (!local_trylock(&s->cpu_sheaves->lock))
> > > > > + if (!pw_trylock_local(&s->cpu_sheaves->lock))
> > > > > return NULL;
> > > >
> > > > alloc_from_pcs() can be called from kmalloc_nolock()/ NMI context.
> > > > I don't remember why exactly local_trylock_t was introduced here instead
> > > > of a per-CPU spinlock_t.
> > >
> > > Probably to save the cost of using atomic operations on locking, and having
> > > about the same restrictions that would allow using local_locks
> > >
> > > > But there should be nothing wrong with a
> > > > trylock on it from NMI as you do here.
> > >
> > > Awesome!
> >
> > The problem is always the unlock which requires full locking and is
> > usually the problem from NMI.
> >
>
> You mean, like, the trylock succeeds in the NMI handle, does the per-cpu
> operations, and then unlock()s?
Yes.
I'm not sure what I was thinking when I wrote it back then. Try lock
with local_trylock() might work under RT from NMI. However the unlock
path (which needs to follow) requires an unconditional lock of the
inner-lock plus a potential wakeup of the waiter.
:
…
> Thanks!
> Leo
Sebastian
^ permalink raw reply
* [PATCH net-next 7/9] net/ipv6: support RFC6724 rule 5.5 via subtrees
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest, David 'equinox' Lamparter,
Jen Linkova
In-Reply-To: <20260714094030.136317-1-equinox@diac24.net>
RFC6724 says:
Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
If SA or SA's prefix is assigned by the selected next-hop that will
be used to send to D and SB or SB's prefix is assigned by a different
next-hop, then prefer SA. Similarly, if SB or SB's prefix is
assigned by the next-hop that will be used to send to D and SA or
SA's prefix is assigned by a different next-hop, then prefer SB.
As it turns out, this behavior is immensely useful in handling
renumbering, multihoming, and source address liveliness checks.
That said, just implementing this as an one-shot in source address
selection is not particularly useful if output routing then meanders
between multiple nexthops. To actually make this work (and work well),
installing source-specific routes for prefixes seen in PIOs is a good
way to go.
To actually select a source address, there's still the route lookup with
an empty source as before, which will select some particular nexthop.
Now, for the various candidate source addresses, this does another
lookup and checks if we got the same nexthop, and prefers those source
addresses - rule 5.5 behavior.
Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
Cc: Lorenzo Colitti <lorenzo@google.com>
Cc: Patrick Rohr <prohr@google.com>
Cc: Maciej Żenczykowski <maze@google.com>
Cc: Jen Linkova <furry13@gmail.com>
---
net/ipv6/addrconf.c | 101 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 100 insertions(+), 1 deletion(-)
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index 0ba46ed518a9..f5c04928aa14 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -1522,6 +1522,7 @@ enum {
IPV6_SADDR_RULE_HOA,
#endif
IPV6_SADDR_RULE_OIF,
+ IPV6_SADDR_RULE_PIO_BY_NEXTHOP,
IPV6_SADDR_RULE_LABEL,
IPV6_SADDR_RULE_PRIVACY,
IPV6_SADDR_RULE_ORCHID,
@@ -1542,13 +1543,14 @@ struct ipv6_saddr_score {
};
struct ipv6_saddr_dst {
- const struct flowi6 *fl6;
+ struct flowi6 *fl6;
const struct dst_entry *dst;
const struct sock *sk;
int ifindex;
int scope;
int label;
unsigned int prefs;
+ struct net *net;
};
static inline int ipv6_saddr_preferred(int type)
@@ -1593,6 +1595,80 @@ static bool ipv6_allow_optimistic_dad(const struct net *net,
#endif
}
+/* "source address is preferable if the chosen nexthop advertised it as a PIO"
+ * => consider 'advertised as a PIO' to be 'the routes for the source's subtree
+ * include one with the same nexthop'
+ *
+ * NB: there is no backtracking in the subtree here, this is intentional -
+ * each prefix seen (and accepted) in PIOs creates essentially a "zone" which
+ * is our search scope.
+ */
+static int ipv6_saddr_rule5p5(struct ipv6_saddr_score *score,
+ struct ipv6_saddr_dst *saddr_dst)
+{
+ const struct rt6_info *rt, *cmp_rt;
+ struct dst_entry *cmp_dst;
+ struct fib6_info *f6i;
+ int ret = 0;
+
+ rt = container_of(saddr_dst->dst, struct rt6_info, dst);
+
+ /* fl6->saddr is ::, cf. check at the top of ipv6_common_get_saddr() */
+ saddr_dst->fl6->saddr = score->ifa->addr;
+ cmp_dst = ip6_route_output(saddr_dst->net, saddr_dst->sk,
+ saddr_dst->fl6);
+ memset(&saddr_dst->fl6->saddr, 0, sizeof(saddr_dst->fl6->saddr));
+
+ if (cmp_dst->error)
+ goto out_release_dst;
+
+ cmp_rt = container_of(cmp_dst, struct rt6_info, dst);
+
+ /* this must work if _any_ nexthop matches; the non-subtree best may
+ * not be in same order as subtree best
+ */
+ for (f6i = rcu_dereference(cmp_rt->from); f6i;
+ f6i = rcu_dereference(f6i->fib6_next)) {
+ struct fib6_nh *f6n = f6i->fib6_nh;
+ struct fib6_info *sibling;
+
+ /* non-subtree route: says nothing about router advertising this source */
+ if (f6i->fib6_src.plen == 0)
+ continue;
+
+ if (f6n->nh_common.nhc_dev != saddr_dst->dst->dev ||
+ f6n->nh_common.nhc_gw_family != AF_INET6)
+ continue;
+
+ if (ipv6_addr_equal(&f6n->nh_common.nhc_gw.ipv6,
+ &rt->rt6i_gateway)) {
+ ret = 1;
+ goto out_release_dst;
+ }
+
+ if (!f6i->fib6_nsiblings)
+ continue;
+
+ list_for_each_entry(sibling, &f6i->fib6_siblings, fib6_siblings) {
+ f6n = sibling->fib6_nh;
+
+ if (f6n->nh_common.nhc_dev != saddr_dst->dst->dev ||
+ f6n->nh_common.nhc_gw_family != AF_INET6)
+ continue;
+
+ if (ipv6_addr_equal(&f6n->nh_common.nhc_gw.ipv6,
+ &rt->rt6i_gateway)) {
+ ret = 1;
+ goto out_release_dst;
+ }
+ }
+ }
+
+out_release_dst:
+ dst_release(cmp_dst);
+ return ret;
+}
+
static int ipv6_get_saddr_eval(struct net *net,
struct ipv6_saddr_score *score,
struct ipv6_saddr_dst *dst,
@@ -1677,6 +1753,24 @@ static int ipv6_get_saddr_eval(struct net *net,
ret = (!dst->ifindex ||
dst->ifindex == score->ifa->idev->dev->ifindex);
break;
+ case IPV6_SADDR_RULE_PIO_BY_NEXTHOP:
+ /* Rule 5.5: Prefer sources advertised by chosen next-hop */
+
+ /* Without subtrees, the source address will make no difference
+ * in the ip6_route_output call in rule5p5. Therefore the rule
+ * 5.5 check becomes useless. This wouldn't result in any
+ * errors, but ip6_route_output isn't free, so if subtrees are
+ * disabled save some cycles by skipping this entirely.
+ *
+ * This is done through subtrees_enabled to have the code
+ * compiled regardless.
+ */
+ if (fib6_routes_require_src(net) && dst->dst
+ && !dst->dst->error)
+ ret = ipv6_saddr_rule5p5(score, dst);
+ else
+ ret = 1;
+ break;
case IPV6_SADDR_RULE_LABEL:
/* Rule 6: Prefer matching label */
ret = ipv6_addr_label(net,
@@ -1835,7 +1929,12 @@ int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst_entry,
int hiscore_idx = 0;
int ret = 0;
+ /* we should never end up here with a non-empty saddr. */
+ if (WARN_ON_ONCE(!ipv6_addr_any(&fl6->saddr)))
+ return 0;
+
dst_type = __ipv6_addr_type(&fl6->daddr);
+ dst.net = net;
dst.fl6 = fl6;
dst.sk = sk;
dst.dst = dst_entry;
--
2.53.0
^ permalink raw reply related
* [PATCH net-next 6/9] net/ipv6: flip IPV6_SUBTREES default to Y
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260714094030.136317-1-equinox@diac24.net>
It's needed for RFC6724 rule 5.5, which as it turns out is more
important than was initially discernible.
Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
net/ipv6/Kconfig | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/net/ipv6/Kconfig b/net/ipv6/Kconfig
index c3806c6ac96f..39659e9b53a3 100644
--- a/net/ipv6/Kconfig
+++ b/net/ipv6/Kconfig
@@ -243,16 +243,26 @@ config IPV6_MULTIPLE_TABLES
config IPV6_SUBTREES
bool "IPv6: source address based routing"
depends on IPV6_MULTIPLE_TABLES
+ default y
help
Enable routing by source address or prefix.
The destination address is still the primary routing key, so mixing
normal and source prefix specific routes in the same routing table
- may sometimes lead to unintended routing behavior. This can be
- avoided by defining different routing tables for the normal and
- source prefix specific routes.
+ may sometimes lead to unintended routing behavior if the order of
+ lookups is misunderstood. To achieve full separation based on source
+ address, different routing tables should be defined for each source
+ prefix.
- If unsure, say N.
+ Subtrees specifically address scenarios where local connectivity
+ between source prefixes is shared and unrestricted, but some
+ destinations (e.g. the default route) have more fine-grained rules.
+
+ This feature is required to correctly support RFC6724 rule 5.5 in
+ source address selection. Rule 5.5 is a necessity for good
+ multihoming, renumbering and source prefix liveliness checks.
+
+ If unsure, say Y.
config IPV6_MROUTE
bool "IPv6: multicast routing"
--
2.53.0
^ permalink raw reply related
* [PATCH net-next 5/9] net/ipv6: drop ip6_route_get_saddr
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260714094030.136317-1-equinox@diac24.net>
It's no longer used anywhere.
Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
include/net/ip6_route.h | 26 --------------------------
1 file changed, 26 deletions(-)
diff --git a/include/net/ip6_route.h b/include/net/ip6_route.h
index 09ffe0f13ce7..fe13cb1d3257 100644
--- a/include/net/ip6_route.h
+++ b/include/net/ip6_route.h
@@ -140,32 +140,6 @@ void rt6_flush_exceptions(struct fib6_info *f6i);
void rt6_age_exceptions(struct fib6_info *f6i, struct fib6_gc_args *gc_args,
unsigned long now);
-static inline int ip6_route_get_saddr(struct net *net, struct fib6_info *f6i,
- const struct in6_addr *daddr,
- unsigned int prefs, int l3mdev_index,
- struct in6_addr *saddr)
-{
- struct net_device *l3mdev;
- struct net_device *dev;
- bool same_vrf;
- int err = 0;
-
- rcu_read_lock();
-
- l3mdev = dev_get_by_index_rcu(net, l3mdev_index);
- if (!f6i || !f6i->fib6_prefsrc.plen || l3mdev)
- dev = f6i ? fib6_info_nh_dev(f6i) : NULL;
- same_vrf = !l3mdev || l3mdev_master_dev_rcu(dev) == l3mdev;
- if (f6i && f6i->fib6_prefsrc.plen && same_vrf)
- *saddr = f6i->fib6_prefsrc.addr;
- else
- err = ipv6_dev_get_saddr(net, same_vrf ? dev : l3mdev, daddr, prefs, saddr);
-
- rcu_read_unlock();
-
- return err;
-}
-
struct rt6_info *rt6_lookup(struct net *net, const struct in6_addr *daddr,
const struct in6_addr *saddr, int oif,
const struct sk_buff *skb, int flags);
--
2.53.0
^ permalink raw reply related
* [PATCH net-next 4/9] net/ipv6: use ipv6_fl_get_saddr in output
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260714094030.136317-1-equinox@diac24.net>
Flatten ip6_route_get_saddr() into ip6_dst_lookup_tail (which really
just means handling fib6_prefsrc), and then replace ipv6_dev_get_saddr
with ipv6_fl_get_saddr to pass down the flow information.
Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
net/ipv6/ip6_output.c | 25 +++++++++++++++++++------
1 file changed, 19 insertions(+), 6 deletions(-)
diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
index 368e4fa3b43c..03b8d42d9df9 100644
--- a/net/ipv6/ip6_output.c
+++ b/net/ipv6/ip6_output.c
@@ -1143,27 +1143,40 @@ static int ip6_dst_lookup_tail(struct net *net, const struct sock *sk,
int flags = 0;
/* The correct way to handle this would be to do
- * ip6_route_get_saddr, and then ip6_route_output; however,
+ * ipv6_fl_get_saddr, and then ip6_route_output; however,
* the route-specific preferred source forces the
- * ip6_route_output call _before_ ip6_route_get_saddr.
+ * ip6_route_output call _before_ ipv6_fl_get_saddr.
*
* In source specific routing (no src=any default route),
* ip6_route_output will fail given src=any saddr, though, so
* that's why we try it again later.
*/
if (ipv6_addr_any(&fl6->saddr)) {
+ struct net_device *l3mdev;
+ struct net_device *dev;
struct fib6_info *from;
struct rt6_info *rt;
+ bool same_vrf;
*dst = ip6_route_output(net, sk, fl6);
rt = (*dst)->error ? NULL : dst_rt6_info(*dst);
rcu_read_lock();
from = rt ? rcu_dereference(rt->from) : NULL;
- err = ip6_route_get_saddr(net, from, &fl6->daddr,
- sk ? READ_ONCE(inet6_sk(sk)->srcprefs) : 0,
- fl6->flowi6_l3mdev,
- &fl6->saddr);
+
+ l3mdev = dev_get_by_index_rcu(net, fl6->flowi6_l3mdev);
+ if (!from || !from->fib6_prefsrc.plen || l3mdev)
+ dev = from ? fib6_info_nh_dev(from) : NULL;
+ same_vrf = !l3mdev || l3mdev_master_dev_rcu(dev) == l3mdev;
+ if (from && from->fib6_prefsrc.plen && same_vrf) {
+ fl6->saddr = from->fib6_prefsrc.addr;
+ err = 0;
+ } else
+ err = ipv6_fl_get_saddr(net, *dst,
+ same_vrf ? dev : l3mdev, sk,
+ sk ? READ_ONCE(inet6_sk(sk)->srcprefs) : 0,
+ fl6);
+
rcu_read_unlock();
if (err)
--
2.53.0
^ permalink raw reply related
* [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260714094030.136317-1-equinox@diac24.net>
RFC6724 rule 5.5 is anything but obvious, especially if trying to do it
well. (RFC8028 and its errata kinda proves the point.)
This documents what exactly the Linux kernel does for RFC6724 rule 5.5,
especially what the routing table needs to look like for it to work.
Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
Documentation/networking/ipv6-addrsel.rst | 75 +++++++++++++++++++++++
MAINTAINERS | 1 +
2 files changed, 76 insertions(+)
create mode 100644 Documentation/networking/ipv6-addrsel.rst
diff --git a/Documentation/networking/ipv6-addrsel.rst b/Documentation/networking/ipv6-addrsel.rst
new file mode 100644
index 000000000000..bed032e69570
--- /dev/null
+++ b/Documentation/networking/ipv6-addrsel.rst
@@ -0,0 +1,75 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================================
+IPv6 source address selection trivia
+====================================
+
+
+RFC6724 rule 5.5 support
+------------------------
+
+RFC6724 rule 5.5 is a very short paragraph in a complex RFC that has turned
+out quite tricky, but also immensely useful in multihoming scenarios. For
+reference, it says:
+
+::
+
+ Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
+ If SA or SA's prefix is assigned by the selected next-hop that will
+ be used to send to D and SB or SB's prefix is assigned by a different
+ next-hop, then prefer SA. Similarly, if SB or SB's prefix is
+ assigned by the next-hop that will be used to send to D and SA or
+ SA's prefix is assigned by a different next-hop, then prefer SB.
+
+The way this works on Linux is as follows:
+
+- prior to any source address selection happening, when receiving a RA, more
+ than the installation of a default route (or ::/128 route) needs to happen:
+ for each PIO, a source-specific (subtree) route is *additionally* installed.
+ The effect of this is that *after* a source address has been selected, one
+ of the routers that advertised it will remain in use (this is *not* RFC 6724
+ related, but rather RFC 8028.) At the same time, these extra routes serve
+ to remember which router advertised what.
+
+- per usual, a route lookup for the IPv6 destination address in consideration
+ is done first. This is passed around in kernel as a dst_entry.
+
+- the source address selection code iterates through the various rules in
+ RFC 6724.
+
+- if/when rule 5.5 is reached, first of all, there is a check if *any* source
+ specific routes exist in the routing table. If there are none, the entire
+ code for 5.5 is skipped because it cannot have any effect, but is not free
+ to execute (can involve multiple routing lookups.) **In applications that
+ use a lot of unbound (e.g. UDP) sockets, installing subtree routes should
+ therefore be avoided to not incur this cost on each source address selection
+ pass.** Alternatively, applications should bind their sockets to a specific
+ source address such that the selection code is never hit.
+
+- if subtree routes do exist, the source address selection code now repeats
+ the routing lookup done before source address selection is entered, except
+ with the source address under consideration filled in. This lookup will hit
+ the subtree routes that were installed (see first item), giving a fresh
+ dst_entry. If the new dst_entry matches the original dst_entry, that means
+ the original router has in fact sent RAs with PIOs for this source address,
+ so it is preferred. Otherwise it is not.
+
+
+There are a few caveats to consider:
+
+- the kernel currently does not create the subtree routes mentioned in the
+ first item. This is a separate work item, partially done at the time of
+ writing this. But this can equally well be performed in userspace processing
+ of RAs, e.g. NetworkManager or plain static configuration.
+
+- since addresses can also be acquired from DHCPv6, even RA/PIO combinations
+ that didn't result in the creation of any addresses (e.g. A=0) should have
+ subtree routes added. Those routes *may* be relevant for DHCPv6-generated
+ addresses.
+
+- the "announce check" lookup does not backtrack. Only the destination prefix
+ that provided the "unspecific" (::/128) match is checked for source prefixes
+ to see what routers advertised what. This means that for e.g. RIOs, subtree
+ routes also have to be created. (Backtracking for this case would further
+ increase the cost of source address selection, for a pretty rare situation
+ that has an easy fix/workaround.)
diff --git a/MAINTAINERS b/MAINTAINERS
index f3218abefd0c..4edf48362a07 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18912,6 +18912,7 @@ F: Documentation/netlink/specs/rt-addr.yaml
F: Documentation/netlink/specs/rt-neigh.yaml
F: Documentation/netlink/specs/rt-route.yaml
F: Documentation/netlink/specs/rt-rule.yaml
+F: Documentation/networking/ipv6-addrsel.rst
F: include/linux/inetdevice.h
F: include/linux/mroute*
F: include/net/addrconf.h
--
2.53.0
^ permalink raw reply related
* [PATCH net-next 8/9] selftests: net: RFC6724 rule 5.5 tests
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260714094030.136317-1-equinox@diac24.net>
This throws a couple of situations at the IPv6 source address selection
logic, primarily to check that RFC6724 rule 5.5 is applied as expected.
This requires `CONFIG_IPV6_SUBTREES=y`, so that's added to
selftests/net/config. (The patchset also changes the default on that,
but arguably better to be explicit.)
Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/config | 1 +
.../net/ipv6_saddr_rfc6724rule5p5.py | 231 ++++++++++++++++++
3 files changed, 233 insertions(+)
create mode 100644 tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 708d960ae07d..c07d2aebad69 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -53,6 +53,7 @@ TEST_PROGS := \
ipv6_flowlabel.sh \
ipv6_force_forwarding.sh \
ipv6_route_update_soft_lockup.sh \
+ ipv6_saddr_rfc6724rule5p5.py \
ipvtap_test.sh \
l2_tos_ttl_inherit.sh \
l2tp.sh \
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index e1ce35c2abbe..b0b66412b9f3 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -48,6 +48,7 @@ CONFIG_IPV6_ROUTER_PREF=y
CONFIG_IPV6_RPL_LWTUNNEL=y
CONFIG_IPV6_SEG6_LWTUNNEL=y
CONFIG_IPV6_SIT=y
+CONFIG_IPV6_SUBTREES=y
CONFIG_IPV6_VTI=y
CONFIG_IPVLAN=m
CONFIG_IPVTAP=m
diff --git a/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py b/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
new file mode 100644
index 000000000000..682b39ba991d
--- /dev/null
+++ b/tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2023-2026 David 'equinox' Lamparter
+"""
+RFC 6724 (IPv6 source address selection) rule 5.5 tests
+
+For reference:
+
+ Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
+ If SA or SA's prefix is assigned by the selected next-hop that will
+ be used to send to D and SB or SB's prefix is assigned by a different
+ next-hop, then prefer SA. Similarly, if SB or SB's prefix is
+ assigned by the next-hop that will be used to send to D and SA or
+ SA's prefix is assigned by a different next-hop, then prefer SB.
+
+(and since it provides the "counterpoint":)
+
+ Rule 8: Use longest matching prefix.
+ If CommonPrefixLen(SA, D) > CommonPrefixLen(SB, D), then prefer SA.
+ Similarly, if CommonPrefixLen(SB, D) > CommonPrefixLen(SA, D), then
+ prefer SB.
+
+Note rule 5.5 was originally optional but made mandatory by
+draft-ietf-6man-rfc6724-update (which at the point of creation of this test
+was already "done" at the IETF but waiting in the RFC editor queue due to a
+blocking dependency.)
+"""
+
+from socket import socket, AF_INET6, SOCK_DGRAM
+from functools import wraps
+from typing import Callable
+
+from lib.py import ksft_run, ksft_exit, ksft_eq
+from lib.py import NetNS, NetNSEnter
+from lib.py import ip
+
+
+def select_addr(dest):
+ """
+ connect() + getsockname() to figure out what was selected as source address
+ """
+ sock = socket(AF_INET6, SOCK_DGRAM, 0)
+ sock.connect((dest, 12345))
+ return sock.getsockname()[0]
+
+
+def in_netns(func: Callable[[], None]) -> Callable[[], None]:
+ """
+ python decorator to put test function in netns
+ """
+
+ @wraps(func)
+ def wrapped() -> None:
+ with NetNS() as testns:
+ with NetNSEnter(str(testns)):
+ func()
+
+ return wrapped
+
+
+@in_netns
+def test_basic() -> None:
+ """
+ Simple & most common case for RFC6724 rule 5.5: multiple default routes
+ """
+ ip("link add type veth")
+ ip("link set veth0 up")
+ ip("link set veth1 up")
+ ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+ ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+ ip("-6 route add default via fe80::1 dev veth0 metric 100")
+ ip("-6 route add default via fe80::2 dev veth0 metric 200")
+ ip("-6 route add default from 2001:db8:10::/48 via fe80::1 dev veth0")
+ ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+ # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+ ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+ ip("-6 route del default via fe80::1 dev veth0 metric 100")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+ ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_nh_obj() -> None:
+ """
+ Same as above, but with nexthop objects for the default route
+
+ NB: The kernel doesn't currently allow nexthop objects for subtree routes.
+ """
+
+ ip("link add type veth")
+ ip("link set veth0 up")
+ ip("link set veth1 up")
+ ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+ ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+
+ # distinct nexthop objects are used, because what matters is the nexthop
+ # itself, not the nexthop object. To cover everything, make a group.
+ ip("nexthop add id 101 via fe80::1 dev veth0")
+ ip("nexthop add id 201 group 101")
+ ip("nexthop add id 102 via fe80::2 dev veth0")
+ ip("nexthop add id 202 group 102")
+
+ ip("-6 route add default nhid 201 metric 100")
+ ip("-6 route add default nhid 202 metric 200")
+ ip("-6 route add default from 2001:db8:10::/48 via fe80::1 dev veth0")
+ ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+ # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+ ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+ ip("-6 route del default nhid 201 metric 100")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+ ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_low_metric() -> None:
+ """
+ Check that subtree routes take effect even if they are higher metric
+
+ For checking that "router advertised prefix", metric is irrelevant. It
+ matters for the initial unspecific lookup to find a nexthop to begin with.
+ (The later source address check lookup doesn't change the nexthop, i.e.
+ the effects of metrics are already done.)
+ """
+ ip("link add type veth")
+ ip("link set veth0 up")
+ ip("link set veth1 up")
+ ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+ ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+ ip("-6 route add default via fe80::1 dev veth0 metric 100")
+ ip("-6 route add default via fe80::2 dev veth0 metric 200")
+ ip("-6 route add default from 2001:db8:10::/48 via fe80::1 dev veth0")
+ ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0 metric 1000")
+ ip("-6 route add default from 2001:db8:1000::/48 via fe80::3 dev veth0 metric 50")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+ # rule 8 would result in the use of the :1001: address, but rule 5.5 applies before.
+ ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:10::1", "rule 5.5 > rule 8")
+
+ ip("-6 route del default via fe80::1 dev veth0 metric 100")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:1000::1", "baseline pass")
+ ksft_eq(select_addr("2001:db8:1001::"), "2001:db8:1000::1", "rule 5.5 > rule 8")
+
+
+@in_netns
+def test_no_subtree() -> None:
+ """
+ Ensure that matching on a non-subtree route doesn't trigger rule 5.5
+
+ (This was non-obviously broken in earlier versions of the implementations,
+ a non-subtree route would still match. Make sure it doesn't break again.)
+ """
+ ip("link add type veth")
+ ip("link set veth0 up")
+ ip("link set veth1 up")
+ ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+ ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+ ip("-6 route add default via fe80::1 dev veth0 metric 100")
+ ip("-6 route add default via fe80::2 dev veth0 metric 200")
+ ip("-6 route add default from 2001:db8:10::/48 via fe80::1 dev veth0")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+ ksft_eq(
+ select_addr("2001:db8:1001::"),
+ "2001:db8:10::1",
+ "rule 5.5 > rule 8, ignoring non-SADR",
+ )
+
+
+@in_netns
+def test_longer() -> None:
+ """
+ Check functionality for non-default destination.
+
+ This is expected to be very rare in actual practice, and doesn't do
+ backtracking (also refer to kernel docs.)
+ """
+ ip("link add type veth")
+ ip("link set veth0 up")
+ ip("link set veth1 up")
+ ip("addr add 2001:db8:10::1/64 dev veth0 nodad")
+ ip("addr add 2001:db8:1000::1/64 dev veth0 nodad")
+ ip("-6 route add default via fe80::1 dev veth0 metric 100")
+ ip("-6 route add 2001:db8:500::/48 via fe80::2 dev veth0 metric 100")
+ ip("-6 route add default from 2001:db8:10::/48 via fe80::1 dev veth0")
+ ip("-6 route add 2001:db8:500::/48 from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+ ksft_eq(select_addr("2001:db8:500::"), "2001:db8:1000::1", "rule 5.5")
+ ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:1000::1", "rule 5.5")
+
+ ip("-6 route add 2001:db8:500:aaa::/64 via fe80::2 dev veth0 metric 100")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+ ksft_eq(select_addr("2001:db8:500::"), "2001:db8:1000::1", "rule 5.5")
+ ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:10::1", "no backtracking")
+
+ ip("-6 route del 2001:db8:500::/48 from 2001:db8:1000::/48 via fe80::2 dev veth0")
+ ip("-6 route add default from 2001:db8:1000::/48 via fe80::2 dev veth0")
+
+ ksft_eq(select_addr("2001:db8:11::"), "2001:db8:10::1", "baseline pass")
+ ksft_eq(select_addr("2001:db8:500::"), "2001:db8:10::1", "no backtracking")
+ ksft_eq(select_addr("2001:db8:500:aaa::"), "2001:db8:10::1", "no backtracking")
+
+
+def main() -> None:
+ """
+ RFC6724 rule 5.5 test driver
+ """
+ ksft_run(
+ [
+ test_basic,
+ test_nh_obj,
+ test_low_metric,
+ test_no_subtree,
+ test_longer,
+ ]
+ )
+ ksft_exit()
+
+
+if __name__ == "__main__":
+ main()
--
2.53.0
^ permalink raw reply related
* [PATCH net-next 3/9] net/ipv6: create ipv6_fl_get_saddr
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260714094030.136317-1-equinox@diac24.net>
This adds passing the relevant flow information as well as selected
nexthop into the source address selection code, to allow the RFC6724
rule 5.5 code to look at its details.
Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
include/net/addrconf.h | 4 ++++
net/ipv6/addrconf.c | 45 +++++++++++++++++++++++++++++++-----------
2 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/include/net/addrconf.h b/include/net/addrconf.h
index 8ced27a8229b..7bc0711b8717 100644
--- a/include/net/addrconf.h
+++ b/include/net/addrconf.h
@@ -142,6 +142,10 @@ struct inet6_ifaddr *ipv6_get_ifaddr(struct net *net,
int ipv6_dev_get_saddr(struct net *net, const struct net_device *dev,
const struct in6_addr *daddr, unsigned int srcprefs,
struct in6_addr *saddr);
+int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst,
+ const struct net_device *dst_dev,
+ const struct sock *sk, unsigned int srcprefs,
+ struct flowi6 *fl6);
int ipv6_get_lladdr(struct net_device *dev, struct in6_addr *addr,
u32 banned_flags);
bool inet_rcv_saddr_equal(const struct sock *sk, const struct sock *sk2,
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index f1fe9ede1edb..0ba46ed518a9 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -1542,7 +1542,9 @@ struct ipv6_saddr_score {
};
struct ipv6_saddr_dst {
- const struct in6_addr *addr;
+ const struct flowi6 *fl6;
+ const struct dst_entry *dst;
+ const struct sock *sk;
int ifindex;
int scope;
int label;
@@ -1619,7 +1621,7 @@ static int ipv6_get_saddr_eval(struct net *net,
break;
case IPV6_SADDR_RULE_LOCAL:
/* Rule 1: Prefer same address */
- ret = ipv6_addr_equal(&score->ifa->addr, dst->addr);
+ ret = ipv6_addr_equal(&score->ifa->addr, &dst->fl6->daddr);
break;
case IPV6_SADDR_RULE_SCOPE:
/* Rule 2: Prefer appropriate scope
@@ -1697,11 +1699,11 @@ static int ipv6_get_saddr_eval(struct net *net,
* non-ORCHID vs non-ORCHID
*/
ret = !(ipv6_addr_orchid(&score->ifa->addr) ^
- ipv6_addr_orchid(dst->addr));
+ ipv6_addr_orchid(&dst->fl6->daddr));
break;
case IPV6_SADDR_RULE_PREFIX:
/* Rule 8: Use longest matching prefix */
- ret = ipv6_addr_diff(&score->ifa->addr, dst->addr);
+ ret = ipv6_addr_diff(&score->ifa->addr, &dst->fl6->daddr);
if (ret > score->ifa->prefix_len)
ret = score->ifa->prefix_len;
score->matchlen = ret;
@@ -1819,9 +1821,10 @@ static int ipv6_get_saddr_master(struct net *net,
return hiscore_idx;
}
-int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
- const struct in6_addr *daddr, unsigned int prefs,
- struct in6_addr *saddr)
+int ipv6_fl_get_saddr(struct net *net, const struct dst_entry *dst_entry,
+ const struct net_device *dst_dev,
+ const struct sock *sk, unsigned int prefs,
+ struct flowi6 *fl6)
{
struct ipv6_saddr_score scores[2], *hiscore;
struct ipv6_saddr_dst dst;
@@ -1832,11 +1835,13 @@ int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
int hiscore_idx = 0;
int ret = 0;
- dst_type = __ipv6_addr_type(daddr);
- dst.addr = daddr;
+ dst_type = __ipv6_addr_type(&fl6->daddr);
+ dst.fl6 = fl6;
+ dst.sk = sk;
+ dst.dst = dst_entry;
dst.ifindex = dst_dev ? dst_dev->ifindex : 0;
dst.scope = __ipv6_addr_src_scope(dst_type);
- dst.label = ipv6_addr_label(net, daddr, dst_type, dst.ifindex);
+ dst.label = ipv6_addr_label(net, &fl6->daddr, dst_type, dst.ifindex);
dst.prefs = prefs;
scores[hiscore_idx].rule = -1;
@@ -1911,11 +1916,29 @@ int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
if (!hiscore->ifa)
ret = -EADDRNOTAVAIL;
else
- *saddr = hiscore->ifa->addr;
+ fl6->saddr = hiscore->ifa->addr;
rcu_read_unlock();
return ret;
}
+EXPORT_SYMBOL(ipv6_fl_get_saddr);
+
+int ipv6_dev_get_saddr(struct net *net, const struct net_device *dst_dev,
+ const struct in6_addr *daddr, unsigned int prefs,
+ struct in6_addr *saddr)
+{
+ struct flowi6 fl6;
+ int ret;
+
+ memset(&fl6, 0, sizeof(fl6));
+ fl6.daddr = *daddr;
+
+ ret = ipv6_fl_get_saddr(net, NULL, dst_dev, NULL, prefs, &fl6);
+ if (!ret)
+ *saddr = fl6.saddr;
+
+ return ret;
+}
EXPORT_SYMBOL(ipv6_dev_get_saddr);
static int __ipv6_get_lladdr(struct inet6_dev *idev, struct in6_addr *addr,
--
2.53.0
^ permalink raw reply related
* [PATCH net-next 2/9] net/ipv6: flatten ip6_route_get_saddr
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest, David 'equinox' Lamparter
In-Reply-To: <20260714094030.136317-1-equinox@diac24.net>
Inline ip6_route_get_saddr()'s functionality in rt6_fill_node(), to
prepare for replacing the former with a dst based function.
NB: the l3mdev handling introduced by 252442f2ae31 "ipv6: fix source
address selection with route leak" is dropped here - the l3mdev ifindex
was a constant 0 on this call site, so that code was in fact dead.
Signed-off-by: David 'equinox' Lamparter <equinox@diac24.net>
---
net/ipv6/route.c | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index a1301334da48..704afe2ddea7 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -5850,9 +5850,19 @@ static int rt6_fill_node(struct net *net, struct sk_buff *skb,
if (nla_put_u32(skb, RTA_IIF, iif))
goto nla_put_failure;
} else if (dest) {
- struct in6_addr saddr_buf;
- if (ip6_route_get_saddr(net, rt, dest, 0, 0, &saddr_buf) == 0 &&
- nla_put_in6_addr(skb, RTA_PREFSRC, &saddr_buf))
+ struct in6_addr saddr_buf, *saddr = NULL;
+
+ if (rt->fib6_prefsrc.plen) {
+ saddr = &rt->fib6_prefsrc.addr;
+ } else {
+ struct net_device *dev = fib6_info_nh_dev(rt);
+
+ if (ipv6_dev_get_saddr(net, dev, dest, 0,
+ &saddr_buf) == 0)
+ saddr = &saddr_buf;
+ }
+
+ if (saddr && nla_put_in6_addr(skb, RTA_PREFSRC, saddr))
goto nla_put_failure;
}
--
2.53.0
^ permalink raw reply related
* [PATCH net-next 0/9] RFC 6724 rule 5.5 support
From: David 'equinox' Lamparter @ 2026-07-14 9:40 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Ido Schimmel
Cc: David Ahern, David S. Miller, Eric Dumazet, Simon Horman,
Jonathan Corbet, Shuah Khan, Fernando Fernandez Mancera,
Lorenzo Colitti, Maciej Żenczykowski, Patrick Rohr, netdev,
linux-doc, linux-kselftest
Hi all,
this patchset implements RFC6724 rule 5.5. For the unaquainted:
Rule 5.5: Prefer addresses in a prefix advertised by the next-hop.
If SA or SA's prefix is assigned by the selected next-hop that will
be used to send to D and SB or SB's prefix is assigned by a different
next-hop, then prefer SA. Similarly, if SB or SB's prefix is
assigned by the next-hop that will be used to send to D and SA or
SA's prefix is assigned by a different next-hop, then prefer SB.
The way this is done is through IPv6 subtree routes. If a router
advertises some prefix in its RA/PIOs, source specific subtree routes
should be created for the default route (and RIOs) installed as a result
of processing that RA.
This may initially sound like a weird way to do it, but for one RFC8028
requires the subtree routes anyway, and also I did try the more obvious
approaches (explicitly tracking it, putting it on the address, putting
it on the neighbor entry) and all of them break in some scenarios.
I've put together a selftest, there's also a rather hacky test suite
created for an IETF hackathon: https://github.com/eqvinox/rule5p5-tests
(it's not specific to this patchset.) I've also been dogfooding these
patches on my personal devices for more than a year.
Rule 5.5 itself has extensive history at the IETF, including changing
from optional to mandatory in the recent 6724 update. It is immensely
useful (really: required) to make multihoming, renumbering and failover
work.
@Jakub you had previously asked me to resubmit the "prep" patches since
it was at a poor time (cf. Fri, Jul 25, 2025 at 05:39:58PM -0700).
(I had tried submitting the preparation bits on its own.)
@Paolo you had looked at the lookup fix:
On Tue, Nov 11, 2025 at 11:13:30AM +0100, Paolo Abeni wrote:
> The patch LGTM, and I agree this should go via net-next, given that it's
> really a corner case and I could miss nasty side-effects.
>
> It looks like you have some testing scenario handy: it would be great to
> include it as a paired self-test; could you please add it?
Cheers,
equi (David)
P.S.: I also happen to be around at netdevconf in Rome, in case anyone
happens to see this and have questions. Of course being at a conference
generally means not looking at random patch mails, so this is mostly
just in case you see the Subject lines or this cover letter. Apologies,
it wasn't possible for me to submit this ahead of the conference.
diffstat:
Documentation/networking/ipv6-addrsel.rst | 75 ++++++++++++++++++++++++++++++++
MAINTAINERS | 1 +
include/net/addrconf.h | 4 ++
include/net/ip6_route.h | 26 ------------
net/ipv6/Kconfig | 18 ++++++--
net/ipv6/addrconf.c | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
net/ipv6/ip6_fib.c | 5 ++-
net/ipv6/ip6_output.c | 26 +++++++++---
net/ipv6/route.c | 16 +++++--
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/config | 1 +
tools/testing/selftests/net/ipv6_saddr_rfc6724rule5p5.py | 231 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 497 insertions(+), 51 deletions(-)
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox