[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Xen-devel] [PATCH v3 06/11] optee: add std call handling



Hello Julien,

Julien Grall writes:

> Hi Volodymyr,
>
> On 18/12/2018 21:11, Volodymyr Babchuk wrote:
>> From: Volodymyr Babchuk <vlad.babchuk@xxxxxxxxx>
>>
>> The main way to communicate with OP-TEE is to issue standard SMCCC
>> call. "Standard" is a SMCCC term and it means that call can be
>> interrupted and OP-TEE can return control to NW before completing
>> the call.
>>
>> In contrast with fast calls, where arguments and return values
>> are passed in registers, standard calls use shared memory. Register
>> pair a1,a2 holds 64-bit PA of command buffer, where all arguments
>> are stored and which is used to return data. OP-TEE internally
>> copies contents of this buffer into own secure memory before accessing
>> and validating any data in command buffer. This is done to make sure
>> that NW will not change contents of the validated parameters.
>>
>> Mediator needs to do the same for number of reasons:
>>
>> 1. To make sure that guest will not change data after validation.
>> 2. To translate IPAs to PAs in the command buffer (this is not done
>>     in this patch).
>> 3. To hide translated address from guest, so it will not be able
>>     to do IPA->PA translation by misusing mediator.
>>
>> During standard call OP-TEE can issue multiple "RPC returns", asking
>> NW to do some work for OP-TEE. NW then issues special call
>> OPTEE_SMC_CALL_RETURN_FROM_RPC to resume handling of the original call.
>> Thus, mediator needs to maintain context for original standard call
>> during multiple SMCCC calls.
>>
>> Standard call is considered complete, when returned value is
>> not a RPC request.
>>
>> Signed-off-by: Volodymyr Babchuk <vlad.babchuk@xxxxxxxxx>
>> ---
>>
>>   Changes from v2:
>>    - renamed struct domain_ctx to struct optee_domain
>>    - fixed coding style
>>    - Now I use access_guest_memory_by_ipa() instead of mappings
>>      to read command buffer
>>    - Added tracking for in flight calls, so guest can't resume
>>      the same call from two CPUs simultaniously
>>
>>   xen/arch/arm/tee/optee.c     | 319 ++++++++++++++++++++++++++++++++++-
>>   xen/include/asm-arm/domain.h |   3 +
>>   2 files changed, 320 insertions(+), 2 deletions(-)
>>
>> diff --git a/xen/arch/arm/tee/optee.c b/xen/arch/arm/tee/optee.c
>> index 584241b03a..dc90e2ed8e 100644
>> --- a/xen/arch/arm/tee/optee.c
>> +++ b/xen/arch/arm/tee/optee.c
>> @@ -12,6 +12,8 @@
>>    */
>>     #include <xen/device_tree.h>
>> +#include <xen/domain_page.h>
>> +#include <xen/guest_access.h>
>>   #include <xen/sched.h>
>>   #include <asm/smccc.h>
>>   #include <asm/tee/tee.h>
>> @@ -22,11 +24,38 @@
>>   /* Client ID 0 is reserved for hypervisor itself */
>>   #define OPTEE_CLIENT_ID(domain) (domain->domain_id + 1)
>>   +/*
>> + * Maximal number of concurrent standard calls from one guest. This
>> + * corresponds to OPTEE configuration option CFG_NUM_THREADS, because
>> + * OP-TEE spawns a thread for every standard call.
>
> Looking at OP-TEE, CFG_NUM_THREADS will vary depending on the
> platform. Is there any way to probe that number of threads from Xen?
Yes, this is per-platform configuration.
Easiest way is to add Fast SMC to OP-TEE that will report this
parameter.
Jens, what do you think about adding additional call?

> In any case, I think we should update the comment to reflect that this
> seems to be the maximum CFG_NUM_THREADS supported by any upstream
> platform.

Actually, OP-TEE protocol have possibility to handle limited number of
threads correctly. OP-TEE can report that all threads are busy and
client will wait for a free one. XEN can do the same, although this is not
implemented in this patch series. But I can add this.

Basically this means that all can work correctly even with
MAX_STD_CALLS==1. It just will be not so efficient.

>> + */
>> +#define MAX_STD_CALLS   16
>> +
>>   #define OPTEE_KNOWN_NSEC_CAPS OPTEE_SMC_NSEC_CAP_UNIPROCESSOR
>>   #define OPTEE_KNOWN_SEC_CAPS (OPTEE_SMC_SEC_CAP_HAVE_RESERVED_SHM | \
>>                                 OPTEE_SMC_SEC_CAP_UNREGISTERED_SHM |  \
>>                                 OPTEE_SMC_SEC_CAP_DYNAMIC_SHM)
>>   +/*
>> + * Call context. OP-TEE can issue multiple RPC returns during one call.
>> + * We need to preserve context during them.
>> + */
>> +struct optee_std_call {
>> +    struct list_head list;
>> +    struct optee_msg_arg *xen_arg;
>> +    paddr_t guest_arg_ipa;
>> +    int optee_thread_id;
>> +    int rpc_op;
>> +    bool in_flight;
>> +};
>> +
>> +/* Domain context */
>> +struct optee_domain {
>> +    struct list_head call_list;
>> +    atomic_t call_count;
>> +    spinlock_t lock;
>> +};
>> +
>>   static bool optee_probe(void)
>>   {
>>       struct dt_device_node *node;
>> @@ -52,6 +81,11 @@ static bool optee_probe(void)
>>   static int optee_enable(struct domain *d)
>>   {
>>       struct arm_smccc_res resp;
>> +    struct optee_domain *ctx;
>> +
>> +    ctx = xzalloc(struct optee_domain);
>> +    if ( !ctx )
>> +        return -ENOMEM;
>>         /*
>>        * Inform OP-TEE about a new guest.
>> @@ -69,9 +103,16 @@ static int optee_enable(struct domain *d)
>>       {
>>           gprintk(XENLOG_WARNING, "Unable to create OPTEE client: rc = 
>> 0x%X\n",
>>                   (uint32_t)resp.a0);
>> +        xfree(ctx);
>>           return -ENODEV;
>>       }
>>   +    INIT_LIST_HEAD(&ctx->call_list);
>> +    atomic_set(&ctx->call_count, 0);
>> +    spin_lock_init(&ctx->lock);
>> +
>> +    d->arch.tee = ctx;
>> +
>>       return 0;
>>   }
>>   @@ -111,9 +152,86 @@ static void set_return(struct cpu_user_regs
>> *regs, uint32_t ret)
>>       set_user_reg(regs, 7, 0);
>>   }
>>   +static struct optee_std_call *allocate_std_call(struct
>> optee_domain *ctx)
>> +{
>> +    struct optee_std_call *call;
>> +    int count;
>> +
>> +    /* Make sure that guest does not execute more than MAX_STD_CALLS */
>> +    count = atomic_add_unless(&ctx->call_count, 1, MAX_STD_CALLS);
>> +    if ( count == MAX_STD_CALLS )
>> +        return NULL;
>> +
>> +    call = xzalloc(struct optee_std_call);
>> +    if ( !call )
>> +    {
>> +        atomic_dec(&ctx->call_count);
>> +        return NULL;
>> +    }
>> +
>> +    call->optee_thread_id = -1;
>> +    call->in_flight = true;
>> +
>> +    spin_lock(&ctx->lock);
>> +    list_add_tail(&call->list, &ctx->call_list);
>> +    spin_unlock(&ctx->lock);
>> +
>> +    return call;
>> +}
>> +
>> +static void free_std_call(struct optee_domain *ctx,
>> +                          struct optee_std_call *call)
>> +{
>> +    atomic_dec(&ctx->call_count);
>> +
>> +    spin_lock(&ctx->lock);
>> +    list_del(&call->list);
>> +    spin_unlock(&ctx->lock);
>> +
>> +    ASSERT(!call->in_flight);
>> +    xfree(call->xen_arg);
>> +    xfree(call);
>> +}
>> +
>> +static struct optee_std_call *get_std_call(struct optee_domain *ctx,
>> +                                           int thread_id)
>> +{
>> +    struct optee_std_call *call;
>> +
>> +    spin_lock(&ctx->lock);
>> +    list_for_each_entry( call, &ctx->call_list, list )
>> +    {
>> +        if ( call->optee_thread_id == thread_id )
>> +        {
>> +            if ( call->in_flight )
>> +            {
>> +                gprintk(XENLOG_WARNING, "Guest tries to execute call which 
>> is already in flight");
>> +                goto out;
>> +            }
>> +            call->in_flight = true;
>> +            spin_unlock(&ctx->lock);
>> +            return call;
>> +        }
>> +    }
>> +out:
>> +    spin_unlock(&ctx->lock);
>> +
>> +    return NULL;
>> +}
>> +
>> +static void put_std_call(struct optee_domain *ctx, struct optee_std_call 
>> *call)
>> +{
>> +    spin_lock(&ctx->lock);
>> +    ASSERT(call->in_flight);
>> +    call->in_flight = false;
>> +    spin_unlock(&ctx->lock);
>> +}
>> +
>>   static void optee_domain_destroy(struct domain *d)
>>   {
>>       struct arm_smccc_res resp;
>> +    struct optee_std_call *call, *call_tmp;
>> +    struct optee_domain *ctx = d->arch.tee;
>>
>>       /* At this time all domain VCPUs should be stopped */
>>   @@ -124,6 +242,199 @@ static void optee_domain_destroy(struct
>> domain *d)
>>        */
>>       arm_smccc_smc(OPTEE_SMC_VM_DESTROYED, OPTEE_CLIENT_ID(d), 0, 0, 0, 0, 
>> 0, 0,
>>                     &resp);
>
> This function can be called without enable and should be
> idempotent. So I woudld check d->arch.tee before and...
Nothing bad will happen if it called without call to enable.

But yes, I need to check for ctx != NULL.

>> +    ASSERT(!spin_is_locked(&ctx->lock));
>> +
>> +    list_for_each_entry_safe( call, call_tmp, &ctx->call_list, list )
>> +        free_std_call(ctx, call);
>> +
>> +    ASSERT(!atomic_read(&ctx->call_count));
>> +
>> +    xfree(d->arch.tee);
>
> use XFREE here.
Oh, looks like I missed this macro introduction. Thank you for pointing
out.

>> +}
>> +
>> +/*
>> + * Copy command buffer into xen memory to:
>> + * 1) Hide translated addresses from guest
>> + * 2) Make sure that guest wouldn't change data in command buffer during 
>> call
>> + */
>> +static bool copy_std_request(struct cpu_user_regs *regs,
>> +                             struct optee_std_call *call)
>> +{
>> +    paddr_t xen_addr;
>> +
>> +    call->guest_arg_ipa = (paddr_t)get_user_reg(regs, 1) << 32 |
>> +                            get_user_reg(regs, 2);
>
> NIT: The indentation looks weird here.
>
>> +
>> +    /*
>> +     * Command buffer should start at page boundary.
>> +     * This is OP-TEE ABI requirement.
>> +     */
>> +    if ( call->guest_arg_ipa & (OPTEE_MSG_NONCONTIG_PAGE_SIZE - 1) )
>> +        return false;
>> +
>> +    call->xen_arg = _xmalloc(OPTEE_MSG_NONCONTIG_PAGE_SIZE,
>> +                             OPTEE_MSG_NONCONTIG_PAGE_SIZE);
>> +    if ( !call->xen_arg )
>> +        return false;
>> +
>> +    BUILD_BUG_ON(OPTEE_MSG_NONCONTIG_PAGE_SIZE > PAGE_SIZE);
>
> As you use _xmalloc, you should not need this. This is only necessary
> if you use alloc_xenheap_page.
>
Yes, right. This is leftover from time when I mapped guest page into
XEN. I'll move it down, to place where I map that page.

> I am wondering whether it is wise to allocate the memory from xenheap
> and not domheap. While on Arm64 (for now) xenheap and domheap are the
> same, on Arm32 they are different. The xenheap is at most 1GB, so
> pretty limited.
Honestly, I have no opinion there. What are limitations of domheap in
this case?


> Furthermore, using domheap would have the advantage to allow in the
> future accounting the allocation to the guest and add more safety
> (there are discussion to make domheap per domain).
>
>> +
>> +    access_guest_memory_by_ipa(current->domain, call->guest_arg_ipa,
>> +                               call->xen_arg, OPTEE_MSG_NONCONTIG_PAGE_SIZE,
>> +                               false);
>
> You need to check the return of access_guest_memory_by_ipa as this
> function can fail.
Will do.

>> +
>> +    xen_addr = virt_to_maddr(call->xen_arg);
>> +
>> +    set_user_reg(regs, 1, xen_addr >> 32);
>> +    set_user_reg(regs, 2, xen_addr & 0xFFFFFFFF);
>> +
>> +    return true;
>> +}
>> +
>> +static void copy_std_request_back(struct optee_domain *ctx,
>> +                                  struct cpu_user_regs *regs,
>> +                                  struct optee_std_call *call)
>
> Can you add a comment on top of the function explaining what it does?
Yes, sure.

"Copy OP-TEE response back into guest's request buffer" will be sufficient?

>> +{
>> +    struct optee_msg_arg *guest_arg;
>> +    struct page_info *page;
>> +    unsigned int i;
>> +    uint32_t attr;
>> +
>> +    /* copy_std_request() validated IPA for us */
>
> Not really, the guest is free to modify the stage-2 mapping on another
> vCPU while this is happening. I agree that the guest will shoot
> himself, but we at least need to not have weird behavior happening.
>
> In that case, I would check that the type is p2m_ram_rw as you don't
> want to write in read-only or foreign mapping.
How I can do this atomically? I.e. what if guest will mangle p2m right
after the check?

> Also, as copy_std_request() and copy_std_request_back may not be
> called in the same "thread" it would be useful if you specify a bit
> more the interaction.
I not sure what do you mean there.

>
>> +    page = get_page_from_gfn(current->domain, 
>> paddr_to_pfn(call->guest_arg_ipa),
>
> Please use gfn_x(gaddr_to_gfn(...)) to clarify this is a gfn. The
> gfn_x will be unnecessary soon with a cleanup that is currently under
> review.
So there are chances, that it will be removed when time will come to
merge this patch series? :)

>
>> +                             NULL, P2M_ALLOC);
>> +    if ( !page )
>> +        return;
>> +
>> +    guest_arg = map_domain_page(page_to_mfn(page));
>
> So here you assume that PAGE_SIZE ==
> OPTEE_MSG_NONCONTIG_PAGE_SIZE. Can you add a BUILD_BUG_ON just above
> (with a comment) so we don't get some nasty surprise with 64K support.
Yes, sure.

> Also, you should be able to use __map_domain_page(page) here.
Oh, thank you for pointing to this macro. Not the best descriptive name,
I must say.

>
>> +
>> +    guest_arg->ret = call->xen_arg->ret;
>> +    guest_arg->ret_origin = call->xen_arg->ret_origin;
>> +    guest_arg->session = call->xen_arg->session;
>
> NIT: newline here please.
>
>> +    for ( i = 0; i < call->xen_arg->num_params; i++ )
>> +    {
>> +        attr = call->xen_arg->params[i].attr;
>> +
>> +        switch ( attr & OPTEE_MSG_ATTR_TYPE_MASK )
>> +        {
>> +        case OPTEE_MSG_ATTR_TYPE_TMEM_OUTPUT:
>> +        case OPTEE_MSG_ATTR_TYPE_TMEM_INOUT:
>> +            guest_arg->params[i].u.tmem.size =
>> +                call->xen_arg->params[i].u.tmem.size;
>> +            continue;
>> +        case OPTEE_MSG_ATTR_TYPE_VALUE_OUTPUT:
>> +        case OPTEE_MSG_ATTR_TYPE_VALUE_INOUT:
>> +            guest_arg->params[i].u.value.a =
>> +                call->xen_arg->params[i].u.value.a;
>> +            guest_arg->params[i].u.value.b =
>> +                call->xen_arg->params[i].u.value.b;
>> +            continue;
>> +        case OPTEE_MSG_ATTR_TYPE_RMEM_OUTPUT:
>> +        case OPTEE_MSG_ATTR_TYPE_RMEM_INOUT:
>> +            guest_arg->params[i].u.rmem.size =
>> +                call->xen_arg->params[i].u.rmem.size;
>> +            continue;
>> +        case OPTEE_MSG_ATTR_TYPE_NONE:
>> +        case OPTEE_MSG_ATTR_TYPE_RMEM_INPUT:
>> +        case OPTEE_MSG_ATTR_TYPE_TMEM_INPUT:
>> +            continue;
>> +        }
>> +    }
>> +
>> +    unmap_domain_page(guest_arg);
>> +    put_page(page);
>> +}
>> +
>> +static void execute_std_call(struct optee_domain *ctx,
>> +                             struct cpu_user_regs *regs,
>> +                             struct optee_std_call *call)
>> +{
>> +    register_t optee_ret;
>> +
>> +    forward_call(regs);
>> +
>> +    optee_ret = get_user_reg(regs, 0);
>> +    if ( OPTEE_SMC_RETURN_IS_RPC(optee_ret) )
>> +    {
>> +        call->optee_thread_id = get_user_reg(regs, 3);
>> +        call->rpc_op = OPTEE_SMC_RETURN_GET_RPC_FUNC(optee_ret);
>> +        put_std_call(ctx, call);
>> +        return;
>> +    }
>> +
>> +    copy_std_request_back(ctx, regs, call);
>> +
>> +    put_std_call(ctx, call);
>> +    free_std_call(ctx, call);
>> +}
>
> Most of the code in this patch is self-explaining, which is quite nice
> :). However, I think this function would require explaining a bit the
> logic. For instance in which case the call will be freed.
Thank you :)
Yes, this depends on if call was completed or it was interrupted due to
RPC, in which case it will be resumed later. I'll add comment.

>> +
>> +static bool handle_std_call(struct optee_domain *ctx,
>> +                            struct cpu_user_regs *regs)
>> +{
>> +    struct optee_std_call *call = allocate_std_call(ctx);
>> +
>> +    if ( !call )
>> +        return false;
>> +
>> +    if ( !copy_std_request(regs, call) )
>> +        goto err;
>> +
>> +    /* Now we can safely examine contents of command buffer */
>> +    if ( OPTEE_MSG_GET_ARG_SIZE(call->xen_arg->num_params) >
>> +         OPTEE_MSG_NONCONTIG_PAGE_SIZE )
>> +        goto err;
>> +
>> +    switch ( call->xen_arg->cmd )
>> +    {
>> +    case OPTEE_MSG_CMD_OPEN_SESSION:
>> +    case OPTEE_MSG_CMD_CLOSE_SESSION:
>> +    case OPTEE_MSG_CMD_INVOKE_COMMAND:
>> +    case OPTEE_MSG_CMD_CANCEL:
>> +    case OPTEE_MSG_CMD_REGISTER_SHM:
>> +    case OPTEE_MSG_CMD_UNREGISTER_SHM:
>> +        break;
>> +    default:
>> +        goto err;
>> +    }
>> +
>> +    execute_std_call(ctx, regs, call);
>> +
>> +    return true;
>
> This function is a bit odd to read. I think it would be more clear if
> you move this code before the break.
Yes, you are right.

>> +
>> +err:
>> +    put_std_call(ctx, call);
>> +    free_std_call(ctx, call);
>> +
>> +    return false;
>> +}
>> +
>> +static bool handle_rpc(struct optee_domain *ctx, struct cpu_user_regs *regs)
>> +{
>> +    struct optee_std_call *call;
>> +    int optee_thread_id = get_user_reg(regs, 3);
>> +
>> +    call = get_std_call(ctx, optee_thread_id);
>> +
>> +    if ( !call )
>> +        return false;
>> +
>> +    switch ( call->rpc_op )
>> +    {
>> +    case OPTEE_SMC_RPC_FUNC_ALLOC:
>> +        /* TODO: Add handling */
>> +        break;
>> +    case OPTEE_SMC_RPC_FUNC_FREE:
>> +        /* TODO: Add handling */
>> +        break;
>> +    case OPTEE_SMC_RPC_FUNC_FOREIGN_INTR:
>> +        break;
>> +    case OPTEE_SMC_RPC_FUNC_CMD:
>> +        /* TODO: Add handling */
>> +        break;
>> +    }
>> +
>> +    execute_std_call(ctx, regs, call);
>> +    return true;
>>   }
>>     static bool handle_exchange_capabilities(struct cpu_user_regs
>> *regs)
>> @@ -161,6 +472,8 @@ static bool handle_exchange_capabilities(struct 
>> cpu_user_regs *regs)
>>     static bool optee_handle_call(struct cpu_user_regs *regs)
>>   {
>> +    struct optee_domain *ctx = current->domain->arch.tee;
>> +
>>       switch ( get_user_reg(regs, 0) )
>>       {
>>       case OPTEE_SMC_CALLS_COUNT:
>> @@ -170,8 +483,6 @@ static bool optee_handle_call(struct cpu_user_regs *regs)
>>       case OPTEE_SMC_FUNCID_GET_OS_REVISION:
>>       case OPTEE_SMC_ENABLE_SHM_CACHE:
>>       case OPTEE_SMC_DISABLE_SHM_CACHE:
>> -    case OPTEE_SMC_CALL_WITH_ARG:
>> -    case OPTEE_SMC_CALL_RETURN_FROM_RPC:
>>           forward_call(regs);
>>           return true;
>>       case OPTEE_SMC_GET_SHM_CONFIG:
>> @@ -180,6 +491,10 @@ static bool optee_handle_call(struct cpu_user_regs 
>> *regs)
>>           return true;
>>       case OPTEE_SMC_EXCHANGE_CAPABILITIES:
>>           return handle_exchange_capabilities(regs);
>> +    case OPTEE_SMC_CALL_WITH_ARG:
>> +        return handle_std_call(ctx, regs);
>> +    case OPTEE_SMC_CALL_RETURN_FROM_RPC:
>> +        return handle_rpc(ctx, regs);
>>       default:
>>           return false;
>>       }
>> diff --git a/xen/include/asm-arm/domain.h b/xen/include/asm-arm/domain.h
>> index 175de44927..88b48697bd 100644
>> --- a/xen/include/asm-arm/domain.h
>> +++ b/xen/include/asm-arm/domain.h
>> @@ -97,6 +97,9 @@ struct arch_domain
>>       struct vpl011 vpl011;
>>   #endif
>>   +#ifdef CONFIG_TEE
>> +    void *tee;
>> +#endif
>
> Did you look whether there are any hole in arch_domain that could be re-used?
I thought about that. But what are chances to find 64bit-wide,
64bit-aligned hole in a structure? If I remember C standard correctly,
there are no reasons for compiler to leave such holes.

I'm talking about aarch64 there, but I assume, that this is true for 
aarch32/armv7.

>>   }  __cacheline_aligned;
>>     struct arch_vcpu
>>
>
> Cheers,


--
Best regards,Volodymyr Babchuk
_______________________________________________
Xen-devel mailing list
Xen-devel@xxxxxxxxxxxxxxxxxxxx
https://lists.xenproject.org/mailman/listinfo/xen-devel

 


Rackspace

Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.