Re: [PATCH v2 08/13] KVM: arm64: Trap & emulate the ITS MAPD command
[email protected] Fri, 07 Aug 2026 16:57:24 +0000
| Newsgroups | dev.linux.lists.kvmarm |
|---|---|
| Message-ID | <[email protected]> |
Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider: - [Critical] The rollback logic in check_table_update is dead code and allows host-driven hypervisor memory compromise. - [Critical] get_num_itt_pages calculates ITT boundaries incorrectly, resulting in underpinning of unaligned ITT tail pages. - [Critical] check_table_update does not enforce physical address alignment for Level 2 tables, allowing hardware ITS memory corruption. - [High] track_pfn_remove skips clearing the tracking entry on early return, permanently leaking dte_entry slots. - [High] Partial failure in process_its_mapd aborts command execution without rolling back EL2 metadata donations. - [High] track_pfn_add leaks hyp_page refcounts when mapping duplicate commands, permanently stranding host memory. - [High] Architecturally legal MAPD unmap commands fail with -EINVAL, permanently stalling the emulated ITS command queue. - [Medium] Re-mapping an existing device to a new ITT leaks the old ITT pages. -- --- Patch [8]: [PATCH v2 08/13] KVM: arm64: Trap & emulate the ITS MAPD command --- commit d27a7f8a2aeffcb0dd24c8e126274036ef6c275e Author: Sebastian Ene <[email protected]> KVM: arm64: Trap & emulate the ITS MAPD command Parse the MAPD command and extract the ITT address to sanitize it. When the command has the valid bit set, share and pin the memory that holds the ITT table with the hypervisor to prevent it from being given to someone else (eg. a VM). Use the pinning as a mechanism to get a grip to the page and to prevent other users of the pKVM API from sharing or donating the page for something else. > diff --git a/arch/arm64/kvm/hyp/nvhe/its_emulate.c b/arch/arm64/kvm/hyp/nvhe/its_emulate.c > index 1ce2f9d8fcf9c..071a08d3602d7 100644 > --- a/arch/arm64/kvm/hyp/nvhe/its_emulate.c > +++ b/arch/arm64/kvm/hyp/nvhe/its_emulate.c [ ... ] > @@ -110,11 +118,236 @@ static int submit_single_cmd(struct its_priv_state *its, bool retry) > return 0; > } > > +static int get_num_itt_pages(struct its_priv_state *its, u8 num_bits) > +{ > + u64 gits_typer, nr_ites; > + size_t sz; > + > + gits_typer = readq_relaxed(its->base + GITS_TYPER); > + if (num_bits > FIELD_GET(GITS_TYPER_IDBITS, gits_typer)) > + return -EINVAL; > + > + nr_ites = BIT_ULL(num_bits + 1); > + sz = nr_ites * (FIELD_GET(GITS_TYPER_ITT_ENTRY_SIZE, gits_typer) + 1); > + sz = max(sz, ITS_ITT_ALIGN) + ITS_ITT_ALIGN - 1; > + > + return PAGE_ALIGN(sz) >> PAGE_SHIFT; > +} [Severity: Critical] Does this calculation miss the intra-page offset of the itt_addr? If a host provides an ITS-aligned but not page-aligned address, sz alone doesn't account for the page boundary crossing. This could result in under-pinning the tail page and allowing it to be mapped to a guest while the hardware ITS writes to it. [ ... ] > +static int check_table_update(struct its_priv_state *its, u32 device_id, u64 type, bool rollback) > +{ > + struct its_baser *table = get_table_from_snapshot(its->host_state, type); > + size_t lvl2_entry_sz, lvl1_table_sz, num_lvl2_entries, num_lvl1_entries; > + u64 *snapshot_table, *original_table; > + u64 prev_entry, new_entry; > + u32 new_entry_index; > + int ret; > + [ ... ] > + snapshot_table = kern_hyp_va(table->base_snapshot); > + original_table = kern_hyp_va(table->base); > + > + /* > + * Look at the host table copy and if the entry hasn't changed the valid > + * bit compared to the original table used by the hardwre, don't update anything. > + */ > + new_entry = snapshot_table[new_entry_index]; > + prev_entry = original_table[new_entry_index]; > + if (!((new_entry ^ prev_entry) & GITS_BASER_VALID)) > + return 0; [Severity: Critical] If a command fails and triggers a rollback, will this early return bypass the rollback logic? Because the forward pass already updated original_table, new_entry and prev_entry will match here. This leaves a window for the host to manipulate snapshot_table and maliciously donate arbitrary hypervisor pages. > + > + /* > + * The host can play nasty tricks with read-modify-write after a > + * rollback is triggered but we still hold on to the original tables > + * which are hyp managed and we don't give back any other page to the > + * host. > + */ > + if (rollback) > + new_entry = new_entry ^ GITS_BASER_VALID; > + > + if (new_entry & GITS_BASER_VALID) > + ret = __pkvm_host_donate_hyp(hyp_phys_to_pfn(new_entry & PHYS_MASK), > + table->psz >> PAGE_SHIFT); [Severity: Critical] Does this pass the physical address to the hypervisor without enforcing the architectural page size alignment (GITS_BASER.Page_Size)? If the host provides an unaligned physical address, the hypervisor will donate based on the unaligned PFN, but the hardware ITS masks the lower bits, potentially accessing memory outside the donated range. > + else > + ret = __pkvm_hyp_donate_host(hyp_phys_to_pfn(prev_entry & PHYS_MASK), > + table->psz >> PAGE_SHIFT); > + if (ret) > + return ret; > + > + original_table[new_entry_index] = new_entry; > + return 0; > +} > + > +static int track_pfn_add(struct its_priv_state *its, u32 device_id, u64 pfn) > +{ > + void *virt = hyp_phys_to_virt(hyp_pfn_to_phys(pfn)); > + struct dte_entry *entries = &its->tracked_entries[0]; > + bool pfn_shared = false; > + int ret; > + int i; > + > + for (i = 0; i < its->num_tracked_entries; i++) { > + if (entries[i].itt_pfn == pfn) { > + if (entries[i].device_id != device_id) { > + pfn_shared = true; > + break; > + } else { > + return hyp_pin_shared_mem(virt, virt + PAGE_SIZE); > + } [Severity: High] If the host issues multiple MAPD commands with Valid=1 for the same device_id and ITT, can this leak hyp_page refcounts? It increments the pin count for a duplicate mapping without adding a new tracking entry. When unmapped, track_pfn_remove unpins only once and clears the tracking entry, which would permanently strand the extra pins. > + } > + } [ ... ] > +static int track_pfn_remove(struct its_priv_state *its, u32 device_id, u64 pfn) > +{ > + void *virt = hyp_phys_to_virt(hyp_pfn_to_phys(pfn)); > + struct dte_entry *entries = &its->tracked_entries[0]; > + int ret; > + int i; > + > + for (i = 0; i < its->num_tracked_entries; i++) { > + if (entries[i].itt_pfn != pfn || entries[i].device_id != device_id) > + continue; > + > + /* To decrement the refcount, first try to unshare it */ > + ret = __pkvm_host_unshare_hyp(pfn); > + if (ret == -EBUSY) { > + hyp_unpin_shared_mem(virt, virt + PAGE_SIZE); > + ret = __pkvm_host_unshare_hyp(pfn); > + if (ret == -EBUSY) > + return 0; [Severity: High] Does this early return skip clearing the tracking entry? If an ITT page is shared by multiple devices and the second unshare returns -EBUSY, bypassing the memset below will permanently leak the dte_entry slot for this device, eventually leading to -ENOSPC. > + > + WARN_ON(ret); > + } > + > + memset(&entries[i], 0, sizeof(struct dte_entry)); > + its->empty_entry = i; > + return 0; > + } > + > + return -EINVAL; > +} [ ... ] > +static int process_its_mapd(struct its_priv_state *its, struct its_cmd_block *cmd, bool rollback) > +{ > + phys_addr_t itt_addr = cmd->raw_cmd[2] & GENMASK(51, 8); > + bool remove = !(cmd->raw_cmd[2] & BIT(63)); > + u8 size = cmd->raw_cmd[1] & GENMASK(4, 0); > + u32 device_id = cmd->raw_cmd[0] >> 32; > + int num_pages, ret; > + u64 itt_pfn; > + > + if (rollback) > + remove = !remove; > + > + itt_pfn = hyp_phys_to_pfn(itt_addr); > + num_pages = get_num_itt_pages(its, size); [Severity: High] The GICv3 specification dictates that when a MAPD command has Valid=0 (an unmap), the ITT_addr and Size fields are IGNORED. If a driver provides zeros for these ignored fields during an unmap, will extracting them here cause track_pfn_remove to fail with -EINVAL, permanently stalling the emulated ITS command queue? > + if (num_pages < 0) > + return num_pages; > + > + ret = check_table_update(its, device_id, GITS_BASER_TYPE_DEVICE, rollback); > + if (ret) > + return ret; > + > + return track_pfn(its, device_id, itt_pfn, num_pages, remove); > +} [Severity: High] If track_pfn() fails here (for example, if the tracking array is full), the error is returned directly. Will this leave the mutations and donations performed by check_table_update() permanently applied without rolling them back or executing the command in hardware? [Severity: Medium] When a MAPD command is issued with Valid=1 for a device_id that is already mapped, but with a new ITT_addr, does this leak the old ITT pages? The new PFNs are tracked, but there doesn't appear to be logic to look up and remove the pre-existing mapping for this device_id. -- Sashiko AI review ยท https://sashiko.dev/#/patchset/[email protected]?part=8