Hi all,
This series fixes the Endpoint Memory Access Descriptor (EMAD) offset calculations and adds the necessary bounds checks for both the core FF-A driver and the pKVM hypervisor.
Prior to FF-A version 1.1, the memory region header didn't specify an explicit offset for the EMADs, leading to the assumption that they immediately follow the header. However, from v1.1 onwards, the specification dictates using the ep_mem_offset` field to determine the start of the memory access array.
The patches in this series address this by: 1. Updating the core `arm_ffa` firmware driver to correctly calculate the descriptor offset using `ep_mem_offset` rather than defaulting to `sizeof(struct ffa_mem_region)`. It also introduces bounds checking against `max_fragsize`. 2. Enhancing the pKVM hypervisor validation logic to no longer strictly enforce that the descriptor strictly follows the header, aligning it with the driver behavior and the FF-A specification, while also ensuring the offset falls within the mailbox buffer bounds.
While addressing these bugs, Sashiko uncovered other issues that were fixed in the same series.
All the patches aside from the first one in optee are urgent fixes as they either impact the hypervisor security or kernel stability.
Changelog ######### v7->v8: - dropped the patch "optee: ffa: Add NULL check in optee_ffa_lend_protmem" from the series as it was queued by Jens - rephrased the commit message for "KVM: arm64: Ensure FFA ranges are page aligned" - introduced a helper macro to get the size of the ffa memory region based on the used version.
v6->v7: - taking the patches from Mostafa and sending a new version with the collected tags - Added overflow checks when doing `ep_offset + emad_size` in the arm ff-a driver - Move the length check before the ffa_mem_reclaim - fix compatibility break with ff-a version 1.0 reported by Sashiko - add one more patch to fix an issue with the FFA_VERSION call that can lead to leaking pKVM stack un-initialized data to a host when -ftrivial-auto-var-init=zero is not used.
v5->v6: - Add fixes tag - Small clean up make variable declaration reverse christmas tree.
v4->v5: - Collect Sudeep Rbs - Add extra patch to check base address alignment. - Remove WARN_ONs in KVM code - Use ffa_emad_size_get() instead of hardcoded size in KVM code.
v3 -> v4: - Address review comments and fix Sashiko bugs
v2 -> v3: - Fixed typo in nvhe/ffa.c (missing sizeof)
v1 -> v2: - For pKVM, removed the strict placement enforcement for `ep_mem_offset` as it is not compliant with the spec, and avoids making assumptions about the driver's memory layout.
Link to: ######## v6: https://lore.kernel.org/all/20260527150236.1978655-1-smostafa@google.com/ v5: https://lore.kernel.org/all/20260526151934.3783707-1-smostafa@google.com/ v4: https://lore.kernel.org/all/20260520204948.2440882-1-smostafa@google.com/ v3: https://lore.kernel.org/all/20260512124442.1899107-1-sebastianene@google.com... v2: https://lore.kernel.org/all/20260430160241.1934777-1-sebastianene@google.com... v1: https://lore.kernel.org/all/ae9KN9nkOgDYJcGP@google.com/T/#t
*** BLURB HERE ***
Mostafa Saleh (3): firmware: arm_ffa: Fix out-of-bound writes in ffa_setup_and_transmit() KVM: arm64: Fix bounds checking in do_ffa_mem_reclaim() KVM: arm64: Ensure FFA ranges are page aligned
Sebastian Ene (3): firmware: arm_ffa: Fix Endpoint Memory Access Descriptor offset calculation KVM: arm64: Validate the offset to the mem access descriptor KVM: arm64: Zero out the stack initialized data in the FFA handler
arch/arm64/kvm/hyp/nvhe/ffa.c | 45 +++++++++++++++++++++---------- drivers/firmware/arm_ffa/driver.c | 25 +++++++++++------ include/linux/arm_ffa.h | 9 ++++++- 3 files changed, 56 insertions(+), 23 deletions(-)
From: Mostafa Saleh smostafa@google.com
Sashiko (locally) reports multiple out-of-bound issues in ffa_setup_and_transmit: 1) Writing ep_mem_access->reserved can write out of bounds for FFA versions < 1.2 as ffa_emad_size_get() returns 16 bytes in that case while reserved has an offset of 24. Instead of zeroing fields, memset the struct to zero first based on the FFA version.
2) Make sure there is enough size to write constituents.
While at it, convert the only sizeof() in the driver that uses a type instead of variable.
Reviewed-by: Sudeep Holla sudeep.holla@kernel.org Fixes: 111a833dc5cb ("firmware: arm_ffa: Set reserved/MBZ fields to zero in the memory descriptors") Signed-off-by: Mostafa Saleh smostafa@google.com Signed-off-by: Sebastian Ene sebastianene@google.com --- drivers/firmware/arm_ffa/driver.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/drivers/firmware/arm_ffa/driver.c b/drivers/firmware/arm_ffa/driver.c index 0f468362c288..01301213896a 100644 --- a/drivers/firmware/arm_ffa/driver.c +++ b/drivers/firmware/arm_ffa/driver.c @@ -725,11 +725,10 @@ ffa_setup_and_transmit(u32 func_id, void *buffer, u32 max_fragsize, for (idx = 0; idx < args->nattrs; idx++) { ep_mem_access = buffer + ffa_mem_desc_offset(buffer, idx, drv_info->version); + memset(ep_mem_access, 0, ffa_emad_size_get(drv_info->version)); ep_mem_access->receiver = args->attrs[idx].receiver; ep_mem_access->attrs = args->attrs[idx].attrs; ep_mem_access->composite_off = composite_offset; - ep_mem_access->flag = 0; - ep_mem_access->reserved = 0; ffa_emad_impdef_value_init(drv_info->version, ep_mem_access->impdef_val, args->attrs[idx].impdef_val); @@ -769,7 +768,7 @@ ffa_setup_and_transmit(u32 func_id, void *buffer, u32 max_fragsize, constituents = buffer; }
- if ((void *)constituents - buffer > max_fragsize) { + if ((void *)constituents + sizeof(*constituents) - buffer > max_fragsize) { pr_err("Memory Region Fragment > Tx Buffer size\n"); return -EFAULT; } @@ -778,7 +777,7 @@ ffa_setup_and_transmit(u32 func_id, void *buffer, u32 max_fragsize, constituents->pg_cnt = args->sg->length / FFA_PAGE_SIZE; constituents->reserved = 0; constituents++; - frag_len += sizeof(struct ffa_mem_region_addr_range); + frag_len += sizeof(*constituents); } while ((args->sg = sg_next(args->sg)));
return ffa_transmit_fragment(func_id, addr, buf_sz, frag_len,
Use the descriptor's `ep_mem_offset` to calculate the start of the endpoint memory access array and to comply with the FF-A spec instead of defaulting to `sizeof(struct ffa_mem_region)`. This requires moving `ffa_mem_region_additional_setup()` earlier in the setup flow. Also, add sanity checks to ensure the calculated descriptor offsets do not exceed `max_fragsize`.
Fixes: 113580530ee7 ("firmware: arm_ffa: Update memory descriptor to support v1.1 format") Reviewed-by: Sudeep Holla sudeep.holla@kernel.org Signed-off-by: Mostafa Saleh smostafa@google.com Signed-off-by: Sebastian Ene sebastianene@google.com --- drivers/firmware/arm_ffa/driver.c | 20 +++++++++++++++----- include/linux/arm_ffa.h | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/drivers/firmware/arm_ffa/driver.c b/drivers/firmware/arm_ffa/driver.c index 01301213896a..7c1b747797b4 100644 --- a/drivers/firmware/arm_ffa/driver.c +++ b/drivers/firmware/arm_ffa/driver.c @@ -713,19 +713,30 @@ ffa_setup_and_transmit(u32 func_id, void *buffer, u32 max_fragsize, struct ffa_composite_mem_region *composite; struct ffa_mem_region_addr_range *constituents; struct ffa_mem_region_attributes *ep_mem_access; - u32 idx, frag_len, length, buf_sz = 0, num_entries = sg_nents(args->sg); + u32 idx, frag_len, length, buf_sz = 0, num_entries = sg_nents(args->sg), ep_offset; + u32 emad_end, emad_size = ffa_emad_size_get(drv_info->version);
mem_region->tag = args->tag; mem_region->flags = args->flags; mem_region->sender_id = drv_info->vm_id; mem_region->attributes = ffa_memory_attributes_get(func_id); + + ffa_mem_region_additional_setup(drv_info->version, mem_region); composite_offset = ffa_mem_desc_offset(buffer, args->nattrs, drv_info->version); + if (composite_offset + sizeof(*composite) > max_fragsize) + return -ENXIO;
for (idx = 0; idx < args->nattrs; idx++) { - ep_mem_access = buffer + - ffa_mem_desc_offset(buffer, idx, drv_info->version); - memset(ep_mem_access, 0, ffa_emad_size_get(drv_info->version)); + ep_offset = ffa_mem_desc_offset(buffer, idx, drv_info->version); + if (check_add_overflow(ep_offset, emad_size, &emad_end)) + return -ENXIO; + + if (emad_end > max_fragsize) + return -ENXIO; + + ep_mem_access = buffer + ep_offset; + memset(ep_mem_access, 0, emad_size); ep_mem_access->receiver = args->attrs[idx].receiver; ep_mem_access->attrs = args->attrs[idx].attrs; ep_mem_access->composite_off = composite_offset; @@ -735,7 +746,6 @@ ffa_setup_and_transmit(u32 func_id, void *buffer, u32 max_fragsize, } mem_region->handle = 0; mem_region->ep_count = args->nattrs; - ffa_mem_region_additional_setup(drv_info->version, mem_region);
composite = buffer + composite_offset; composite->total_pg_cnt = ffa_get_num_pages_sg(args->sg); diff --git a/include/linux/arm_ffa.h b/include/linux/arm_ffa.h index 17eca3dfc59e..033c630b271b 100644 --- a/include/linux/arm_ffa.h +++ b/include/linux/arm_ffa.h @@ -445,7 +445,7 @@ ffa_mem_desc_offset(struct ffa_mem_region *buf, int count, u32 ffa_version) if (!FFA_MEM_REGION_HAS_EP_MEM_OFFSET(ffa_version)) offset += offsetof(struct ffa_mem_region, ep_mem_offset); else - offset += sizeof(struct ffa_mem_region); + offset += buf->ep_mem_offset;
return offset; }
From: Mostafa Saleh smostafa@google.com
Sashiko (locally) reports out of bound write possiblity if SPMD returns an invalid data.
While SPMD is considered trusted, pKVM does some basic checks, for offset to be less than or equal len.
However, that is incorrect as even if the offset is smaller than len pKVM can still access out of bound memory in the next ffa_host_unshare_ranges().
Split this check into 2: 1- Check that the fixed portion of the descriptor fits. 2- After getting reg, check the variable array size addr_range_cnt fits.
Also, drop the WARN_ONs as that will panic the kernel and in the next checks there are no WARNs, so that makes it consistent.
Fixes: 0a9f15fd5674 ("KVM: arm64: pkvm: Add support for fragmented FF-A descriptors") Signed-off-by: Mostafa Saleh smostafa@google.com Reviewed-by: Vincent Donnefort vdonnefort@google.com Signed-off-by: Sebastian Ene sebastianene@google.com --- arch/arm64/kvm/hyp/nvhe/ffa.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/arch/arm64/kvm/hyp/nvhe/ffa.c b/arch/arm64/kvm/hyp/nvhe/ffa.c index 1af722771178..2d211661952e 100644 --- a/arch/arm64/kvm/hyp/nvhe/ffa.c +++ b/arch/arm64/kvm/hyp/nvhe/ffa.c @@ -607,8 +607,8 @@ static void do_ffa_mem_reclaim(struct arm_smccc_1_2_regs *res, * check that we end up with something that doesn't look _completely_ * bogus. */ - if (WARN_ON(offset > len || - fraglen > KVM_FFA_MBOX_NR_PAGES * PAGE_SIZE)) { + if (offset + CONSTITUENTS_OFFSET(0) > len || + fraglen > KVM_FFA_MBOX_NR_PAGES * PAGE_SIZE) { ret = FFA_RET_ABORTED; ffa_rx_release(res); goto out_unlock; @@ -636,11 +636,17 @@ static void do_ffa_mem_reclaim(struct arm_smccc_1_2_regs *res, ffa_rx_release(res); }
+ reg = (void *)buf + offset; + if (offset + CONSTITUENTS_OFFSET(reg->addr_range_cnt) > len) { + ret = FFA_RET_ABORTED; + ffa_rx_release(res); + goto out_unlock; + } + ffa_mem_reclaim(res, handle_lo, handle_hi, flags); if (res->a0 != FFA_SUCCESS) goto out_unlock;
- reg = (void *)buf + offset; /* If the SPMD was happy, then we should be too. */ WARN_ON(ffa_host_unshare_ranges(reg->constituents, reg->addr_range_cnt));
Prevent the pKVM hypervisor from making assumptions that the endpoint memory access descriptor (EMAD) comes right after the FF-A memory region header. Prior to FF-A version 1.1 the header of the memory region didn't contain an offset to the endpoint memory access descriptor. The layout of a memory transaction looks like this from 1.1 onward: Type | Field name | Offset [ Header | ffa_mem_region | 0 EMAD 1 | ffa_mem_region_attributes) | ffa_mem_region.ep_mem_offset ] Verify that the offset to the first endpoint memory access descriptor is within the mailbox buffer bounds.
Also, fix one hardcoded sizeof(struct ffa_mem_region_attributes) that should be replaced ffa_emad_size_get() for compatibility with FFA v1.0.
Fixes: 42fb33dde42b ("KVM: arm64: Use FF-A 1.1 with pKVM") Signed-off-by: Mostafa Saleh smostafa@google.com Signed-off-by: Sebastian Ene sebastianene@google.com --- arch/arm64/kvm/hyp/nvhe/ffa.c | 27 +++++++++++++++++++-------- include/linux/arm_ffa.h | 7 +++++++ 2 files changed, 26 insertions(+), 8 deletions(-)
diff --git a/arch/arm64/kvm/hyp/nvhe/ffa.c b/arch/arm64/kvm/hyp/nvhe/ffa.c index 2d211661952e..14aa37846059 100644 --- a/arch/arm64/kvm/hyp/nvhe/ffa.c +++ b/arch/arm64/kvm/hyp/nvhe/ffa.c @@ -476,11 +476,12 @@ static void __do_ffa_mem_xfer(const u64 func_id, DECLARE_REG(u32, fraglen, ctxt, 2); DECLARE_REG(u64, addr_mbz, ctxt, 3); DECLARE_REG(u32, npages_mbz, ctxt, 4); + u32 offset, nr_ranges, checked_offset, em_mem_access_off; struct ffa_mem_region_attributes *ep_mem_access; struct ffa_composite_mem_region *reg; struct ffa_mem_region *buf; - u32 offset, nr_ranges, checked_offset; int ret = 0; + size_t mem_region_len = FFA_MEM_REGION_SZ(hyp_ffa_version);
if (addr_mbz || npages_mbz || fraglen > len || fraglen > KVM_FFA_MBOX_NR_PAGES * PAGE_SIZE) { @@ -488,8 +489,7 @@ static void __do_ffa_mem_xfer(const u64 func_id, goto out; }
- if (fraglen < sizeof(struct ffa_mem_region) + - sizeof(struct ffa_mem_region_attributes)) { + if (fraglen < mem_region_len + ffa_emad_size_get(hyp_ffa_version)) { ret = FFA_RET_INVALID_PARAMETERS; goto out; } @@ -508,8 +508,13 @@ static void __do_ffa_mem_xfer(const u64 func_id, buf = hyp_buffers.tx; memcpy(buf, host_buffers.tx, fraglen);
- ep_mem_access = (void *)buf + - ffa_mem_desc_offset(buf, 0, hyp_ffa_version); + em_mem_access_off = ffa_mem_desc_offset(buf, 0, hyp_ffa_version); + if ((u64)em_mem_access_off + ffa_emad_size_get(hyp_ffa_version) > fraglen) { + ret = FFA_RET_INVALID_PARAMETERS; + goto out_unlock; + } + + ep_mem_access = (void *)buf + em_mem_access_off; offset = ep_mem_access->composite_off; if (!offset || buf->ep_count != 1 || buf->sender_id != HOST_FFA_ID) { ret = FFA_RET_INVALID_PARAMETERS; @@ -574,9 +579,9 @@ static void do_ffa_mem_reclaim(struct arm_smccc_1_2_regs *res, DECLARE_REG(u32, handle_lo, ctxt, 1); DECLARE_REG(u32, handle_hi, ctxt, 2); DECLARE_REG(u32, flags, ctxt, 3); + u32 offset, len, fraglen, fragoff, em_mem_access_off; struct ffa_mem_region_attributes *ep_mem_access; struct ffa_composite_mem_region *reg; - u32 offset, len, fraglen, fragoff; struct ffa_mem_region *buf; int ret = 0; u64 handle; @@ -599,8 +604,14 @@ static void do_ffa_mem_reclaim(struct arm_smccc_1_2_regs *res, len = res->a1; fraglen = res->a2;
- ep_mem_access = (void *)buf + - ffa_mem_desc_offset(buf, 0, hyp_ffa_version); + em_mem_access_off = ffa_mem_desc_offset(buf, 0, hyp_ffa_version); + if ((u64)em_mem_access_off + ffa_emad_size_get(hyp_ffa_version) > fraglen) { + ret = FFA_RET_INVALID_PARAMETERS; + ffa_rx_release(res); + goto out_unlock; + } + + ep_mem_access = (void *)buf + em_mem_access_off; offset = ep_mem_access->composite_off; /* * We can trust the SPMD to get this right, but let's at least diff --git a/include/linux/arm_ffa.h b/include/linux/arm_ffa.h index 033c630b271b..e71d83ee0aef 100644 --- a/include/linux/arm_ffa.h +++ b/include/linux/arm_ffa.h @@ -421,6 +421,13 @@ struct ffa_mem_region { #define FFA_EMAD_HAS_IMPDEF_FIELD(version) ((version) >= FFA_VERSION_1_2) #define FFA_MEM_REGION_HAS_EP_MEM_OFFSET(version) ((version) > FFA_VERSION_1_0)
+/* The layout changed from FFA_VERSION_1_0 and the region includes an + * ep_mem_offset. + */ +#define FFA_MEM_REGION_SZ(version) (!FFA_MEM_REGION_HAS_EP_MEM_OFFSET((version)) ?\ + offsetof(struct ffa_mem_region, ep_mem_offset) :\ + sizeof(struct ffa_mem_region)) + static inline u32 ffa_emad_size_get(u32 ffa_version) { u32 sz;
From: Mostafa Saleh smostafa@google.com
Harden the check for the constituent memory region page alignment to prevent over-sharing when the negotiated FFA_PAGE_SIZE size is smaller than the system PAGE_SIZE. At the moment we only check that the size of the range is page aligned, and truncate the address to the page boundary which can annotate more memory than needed as being used by the FF-A.
Fixes: 436090001776 ("KVM: arm64: Handle FFA_MEM_SHARE calls from the host") Signed-off-by: Mostafa Saleh smostafa@google.com Reviewed-by: Vincent Donnefort vdonnefort@google.com Signed-off-by: Sebastian Ene sebastianene@google.com --- arch/arm64/kvm/hyp/nvhe/ffa.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/arch/arm64/kvm/hyp/nvhe/ffa.c b/arch/arm64/kvm/hyp/nvhe/ffa.c index 14aa37846059..bb01d8f83056 100644 --- a/arch/arm64/kvm/hyp/nvhe/ffa.c +++ b/arch/arm64/kvm/hyp/nvhe/ffa.c @@ -352,7 +352,7 @@ static u32 __ffa_host_share_ranges(struct ffa_mem_region_addr_range *ranges, u64 sz = (u64)range->pg_cnt * FFA_PAGE_SIZE; u64 pfn = hyp_phys_to_pfn(range->address);
- if (!PAGE_ALIGNED(sz)) + if (!PAGE_ALIGNED(sz | range->address)) break;
if (__pkvm_host_share_ffa(pfn, sz / PAGE_SIZE)) @@ -372,7 +372,7 @@ static u32 __ffa_host_unshare_ranges(struct ffa_mem_region_addr_range *ranges, u64 sz = (u64)range->pg_cnt * FFA_PAGE_SIZE; u64 pfn = hyp_phys_to_pfn(range->address);
- if (!PAGE_ALIGNED(sz)) + if (!PAGE_ALIGNED(sz | range->address)) break;
if (__pkvm_host_unshare_ffa(pfn, sz / PAGE_SIZE))
Don't leak hypervisor stack data when using the FFA_VERSION call. When the compiler doesn't support -ftrivial-auto-var-init=zero option we need to zero out the stack initialized variable before returning data to the host caller.
Closes: https://lore.kernel.org/all/20260616160016.C62C81F000E9@smtp.kernel.org/
Reported-by: Sashiko AI sashiko-bot@kernel.org Fixes: c9c012625e12 ("KVM: arm64: Trap FFA_VERSION host call in pKVM") Reviewed-by: Vincent Donnefort vdonnefort@google.com Link: https://lore.kernel.org/all/20260616160016.C62C81F000E9@smtp.kernel.org/ Signed-off-by: Sebastian Ene sebastianene@google.com --- arch/arm64/kvm/hyp/nvhe/ffa.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/arch/arm64/kvm/hyp/nvhe/ffa.c b/arch/arm64/kvm/hyp/nvhe/ffa.c index bb01d8f83056..7407bc4b547a 100644 --- a/arch/arm64/kvm/hyp/nvhe/ffa.c +++ b/arch/arm64/kvm/hyp/nvhe/ffa.c @@ -881,7 +881,7 @@ static void do_ffa_part_get(struct arm_smccc_1_2_regs *res,
bool kvm_host_ffa_handler(struct kvm_cpu_context *host_ctxt, u32 func_id) { - struct arm_smccc_1_2_regs res; + struct arm_smccc_1_2_regs res = {0};
/* * There's no way we can tell what a non-standard SMC call might
op-tee@lists.trustedfirmware.org