[PATCH v1 1/5] iommu/arm-smmu-v3-iommufd: Reject unsupported bits in invalidation commands
From: Nicolin Chen
Date: Mon Jun 29 2026 - 17:17:15 EST
The arm_vsmmu_cache_invalidate() op hands a guest's invalidation commands
to the trusted main command queue after enforcing only the VMID or the SID,
and passes the rest of the command through to the queue unchanged.
That lets a guest set bits the host never meant to forward, in two ways. A
bit can take the command out of the guest's own scope: the ATC_INV Global
bit, for one, makes the SMMU ignore the SID and invalidate the ATC of every
device, not just the guest's. A reserved or undefined bit instead makes the
command malformed; per the Arm SMMUv3 specification, in its section 4.1.3
"Command errors", a CERROR_ILL is raised, among other cases, when:
A valid command opcode is used and a Reserved or undefined field is
optionally detected as non-zero, which results in the command being
treated as malformed.
Restrict each opcode to the fields that the driver supports and reject the
command with -EIO if it sets any other bit, before the command reaches the
queue. This keeps a guest scoped to its own devices and stops the host from
forwarding any bit whose meaning it does not control.
Some fields and whole opcodes are legal only on an SMMU that implements the
matching feature, so accept them conditionally. The NUM, SCALE and TG range
fields need FEAT_RANGE_INV. The ATC_INV opcode needs FEAT_ATS. Per the same
specification's section 4.5 "ATS and PRI", CMD_ATC_INV is ILLEGAL when:
SMMU_IDR0.ATS == 0 and this command is issued on a Non-secure or Secure
Command queue.
The SSV and SSID substream fields require a non-zero ssid_bits, so without
substream support setting them is not illegal but CONSTRAINED UNPREDICTABLE,
which a guest should not be able to provoke.
Fixes: d68beb276ba2 ("iommu/arm-smmu-v3: Support IOMMU_HWPT_INVALIDATE using a VIOMMU object")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Nicolin Chen <nicolinc@xxxxxxxxxx>
---
.../arm/arm-smmu-v3/arm-smmu-v3-iommufd.c | 58 +++++++++++++++++++
1 file changed, 58 insertions(+)
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 1e9f7d2de3441..393d69783225c 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
@@ -315,10 +315,64 @@ struct arm_vsmmu_invalidation_cmd {
static int arm_vsmmu_convert_user_cmd(struct arm_vsmmu *vsmmu,
struct arm_vsmmu_invalidation_cmd *cmd)
{
+ u64 allowed[2] = { CMDQ_0_OP };
+
/* Commands are le64 stored in u64 */
cmd->cmd.data[0] = le64_to_cpu(cmd->ucmd.cmd[0]);
cmd->cmd.data[1] = le64_to_cpu(cmd->ucmd.cmd[1]);
+ /* Collect the fields userspace is allowed to set for each opcode */
+ switch (cmd->cmd.data[0] & CMDQ_0_OP) {
+ case CMDQ_OP_TLBI_NH_VA:
+ allowed[0] |= CMDQ_TLBI_0_ASID;
+ fallthrough;
+ case CMDQ_OP_TLBI_NH_VAA:
+ allowed[0] |= CMDQ_TLBI_0_VMID;
+ allowed[1] |= CMDQ_TLBI_1_LEAF | CMDQ_TLBI_1_TTL |
+ CMDQ_TLBI_1_VA_MASK;
+ /* NUM/SCALE/TG are range fields gated on FEAT_RANGE_INV */
+ if (vsmmu->smmu->features & ARM_SMMU_FEAT_RANGE_INV) {
+ allowed[0] |= CMDQ_TLBI_0_NUM | CMDQ_TLBI_0_SCALE;
+ allowed[1] |= CMDQ_TLBI_1_TG;
+ }
+ break;
+ case CMDQ_OP_TLBI_NH_ASID:
+ allowed[0] |= CMDQ_TLBI_0_ASID;
+ fallthrough;
+ case CMDQ_OP_TLBI_NH_ALL:
+ allowed[0] |= CMDQ_TLBI_0_VMID;
+ break;
+ case CMDQ_OP_ATC_INV:
+ /*
+ * Exclude the Global bit: it makes the SMMU ignore the SID and
+ * invalidate the ATC of every device, not just the guest's.
+ */
+ allowed[0] |= CMDQ_ATC_0_SID;
+ allowed[1] |= CMDQ_ATC_1_SIZE | CMDQ_ATC_1_ADDR_MASK;
+ /* SSV/SSID require substream support */
+ if (vsmmu->smmu->ssid_bits)
+ allowed[0] |= CMDQ_0_SSV | CMDQ_ATC_0_SSID;
+ break;
+ case CMDQ_OP_CFGI_CD:
+ allowed[1] |= CMDQ_CFGI_1_LEAF;
+ /* No SSV for CFGI_CD; SSID requires substream support */
+ if (vsmmu->smmu->ssid_bits)
+ allowed[0] |= CMDQ_CFGI_0_SSID;
+ fallthrough;
+ case CMDQ_OP_CFGI_CD_ALL:
+ allowed[0] |= CMDQ_CFGI_0_SID;
+ break;
+ }
+
+ /*
+ * Reject any other bit, e.g. a RES0 bit or a Secure bit, before the
+ * command reaches the trusted main cmdq, so a guest cannot wedge the
+ * shared queue for every device with a CERROR_ILL.
+ */
+ if ((cmd->cmd.data[0] & ~allowed[0]) ||
+ (cmd->cmd.data[1] & ~allowed[1]))
+ return -EIO;
+
switch (cmd->cmd.data[0] & CMDQ_0_OP) {
case CMDQ_OP_TLBI_NSNH_ALL:
/* Convert to NH_ALL */
@@ -334,6 +388,10 @@ static int arm_vsmmu_convert_user_cmd(struct arm_vsmmu *vsmmu,
cmd->cmd.data[0] |= FIELD_PREP(CMDQ_TLBI_0_VMID, vsmmu->vmid);
break;
case CMDQ_OP_ATC_INV:
+ /* ATC_INV is illegal unless the SMMU implements ATS */
+ if (!(vsmmu->smmu->features & ARM_SMMU_FEAT_ATS))
+ return -EIO;
+ fallthrough;
case CMDQ_OP_CFGI_CD:
case CMDQ_OP_CFGI_CD_ALL: {
u32 sid, vsid = FIELD_GET(CMDQ_CFGI_0_SID, cmd->cmd.data[0]);
--
2.43.0