Re: [RFC v2 05/32] x86/tdx: Add __tdcall() and __tdvmcall() helper functions

From: Dave Hansen
Date: Mon Apr 26 2021 - 16:32:49 EST


> +/*
> + * Expose registers R10-R15 to VMM (for bitfield info
> + * refer to TDX GHCI specification).
> + */
> +#define TDVMCALL_EXPOSE_REGS_MASK 0xfc00

Why can't we do:

#define TDC_R10 BIT(18)
#define TDC_R11 BIT(19)

and:

#define TDVMCALL_EXPOSE_REGS_MASK (TDX_R10 | TDX_R11 | TDX_R12 ...

or at least:

#define TDVMCALL_EXPOSE_REGS_MASK BIT(18) | BIT(19) ...

?

> +/*
> + * TDX guests use the TDCALL instruction to make
> + * hypercalls to the VMM. It is supported in
> + * Binutils >= 2.36.
> + */
> +#define tdcall .byte 0x66,0x0f,0x01,0xcc
> +
> +/*
> + * __tdcall() - Used to communicate with the TDX module

Why is this function here? What does it do? Why do we need it?

I'd like this to actually talk about doing impedance matching between
the function call and TDCALL ABIs.

> + * @arg1 (RDI) - TDCALL Leaf ID
> + * @arg2 (RSI) - Input parameter 1 passed to TDX module
> + * via register RCX
> + * @arg2 (RDX) - Input parameter 2 passed to TDX module
> + * via register RDX
> + * @arg3 (RCX) - Input parameter 3 passed to TDX module
> + * via register R8
> + * @arg4 (R8) - Input parameter 4 passed to TDX module
> + * via register R9

The unnecessary repitition and verbosity actually make this harder to
read. This looks like it was easy to write, but not much effort is
being made to make it easy to consume. Could you please apply some
consideration to making it more readable?


> + * @arg5 (R9) - struct tdcall_output pointer
> + *
> + * @out - Return status of tdcall via RAX.

Don't comments usually just say "returns ... foo"? Also, the @params
usually refer to *REAL* variable names. Where the heck does "out" come
from? Why are you even putting argX? Shouldn't these be @'s be their
literal function argument names?

@rdi - Input parameter, moved to RCX

> + * NOTE: This function should only used for non TDVMCALL
> + * use cases
> + */
> +SYM_FUNC_START(__tdcall)
> + FRAME_BEGIN
> +
> + /* Save non-volatile GPRs that are exposed to the VMM. */
> + push %r15
> + push %r14
> + push %r13
> + push %r12

Why do we have to save these? Because they might be clobbered? If so,
let's say *THAT* instead of just "exposed". "Exposed" could mean "VMM
can read".

Also, this just told me that this function can't be used to talk to the
VMM. Why is this talking about exposure to the VMM?

> + /* Move TDCALL Leaf ID to RAX */
> + mov %rdi, %rax
> + /* Move output pointer to R12 */
> + mov %r9, %r12

I thought 'struct tdcall_output' was a purely software construct. Why
are we passing a pointer to it into TDCALL?

> + /* Move input param 4 to R9 */
> + mov %r8, %r9
> + /* Move input param 3 to R8 */
> + mov %rcx, %r8
> + /* Leave input param 2 in RDX */
> + /* Move input param 1 to RCX */
> + mov %rsi, %rcx

With a little work, this can be made a *LOT* more readable:

/* Mangle function call ABI into TDCALL ABI: */
mov %rdi, %rax /* Move TDCALL Leaf ID to RAX */
mov %r9, %r12 /* Move output pointer to R12 */
mov %r8, %r9 /* Move input 4 to R9 */
mov %rcx, %r8 /* Move input 3 to R8 */
mov %rsi, %rcx /* Move input 1 to RCX */
/* Leave input param 2 in RDX */


> + tdcall
> +
> + /* Check for TDCALL success: 0 - Successful, otherwise failed */
> + test %rax, %rax
> + jnz 1f
> +
> + /* Check for a TDCALL output struct */
> + test %r12, %r12
> + jz 1f

Does some universal status come back in r12? Aren't we dealing with a
VMM/SEAM-controlled register here? Isn't this dangerous?

> + /* Copy TDCALL result registers to output struct: */
> + movq %rcx, TDCALL_rcx(%r12)
> + movq %rdx, TDCALL_rdx(%r12)
> + movq %r8, TDCALL_r8(%r12)
> + movq %r9, TDCALL_r9(%r12)
> + movq %r10, TDCALL_r10(%r12)
> + movq %r11, TDCALL_r11(%r12)
> +1:
> + /* Zero out registers exposed to the TDX Module. */
> + xor %rcx, %rcx
> + xor %rdx, %rdx
> + xor %r8d, %r8d
> + xor %r9d, %r9d
> + xor %r10d, %r10d
> + xor %r11d, %r11d

... why?

> + /* Restore non-volatile GPRs that are exposed to the VMM. */
> + pop %r12
> + pop %r13
> + pop %r14
> + pop %r15
> +
> + FRAME_END
> + ret
> +SYM_FUNC_END(__tdcall)
> +
> +/*
> + * do_tdvmcall() - Used to communicate with the VMM.
> + *
> + * @arg1 (RDI) - TDVMCALL function, e.g. exit reason
> + * @arg2 (RSI) - Input parameter 1 passed to VMM
> + * via register R12
> + * @arg3 (RDX) - Input parameter 2 passed to VMM
> + * via register R13
> + * @arg4 (RCX) - Input parameter 3 passed to VMM
> + * via register R14
> + * @arg5 (R8) - Input parameter 4 passed to VMM
> + * via register R15
> + * @arg6 (R9) - struct tdvmcall_output pointer
> + *
> + * @out - Return status of tdvmcall(R10) via RAX.
> + *
> + */

Same comments on the sparse comment style.

> +SYM_CODE_START_LOCAL(do_tdvmcall)
> + FRAME_BEGIN
> +
> + /* Save non-volatile GPRs that are exposed to the VMM. */
> + push %r15
> + push %r14
> + push %r13
> + push %r12
> +
> + /* Set TDCALL leaf ID to TDVMCALL (0) in RAX */

I think there needs to be some discussion of what TDCALL and TDVMCALL
are. They are named too similarly not to do so.

> + xor %eax, %eax
> + /* Move TDVMCALL function id (1st argument) to R11 */
> + mov %rdi, %r11> + /* Move Input parameter 1-4 to R12-R15 */
> + mov %rsi, %r12
> + mov %rdx, %r13
> + mov %rcx, %r14
> + mov %r8, %r15
> + /* Leave tdvmcall output pointer in R9 */
> +
> + /*
> + * Value of RCX is used by the TDX Module to determine which
> + * registers are exposed to VMM. Each bit in RCX represents a
> + * register id. You can find the bitmap details from TDX GHCI
> + * spec.
> + */

This doesn't belong here. Put it along with the
TDVMCALL_EXPOSE_REGS_MASK, please.

> + movl $TDVMCALL_EXPOSE_REGS_MASK, %ecx
> +
> + tdcall
> +
> + /*
> + * Check for TDCALL success: 0 - Successful, otherwise failed.
> + * If failed, there is an issue with TDX Module which is fatal
> + * for the guest. So panic.
> + */
> + test %rax, %rax
> + jnz 2f

So, just to be clear: %RAX is under the control of the SEAM module. The
VMM has no control over it. Right?

Shouldn't we say that explicitly?

> + /* Move TDVMCALL success/failure to RAX to return to user */
> + mov %r10, %rax
> +
> + /* Check for TDVMCALL success: 0 - Successful, otherwise failed */
> + test %rax, %rax
> + jnz 1f
> +
> + /* Check for a TDVMCALL output struct */
> + test %r9, %r9
> + jz 1f

I'd also include a note that %r9 was neither writable nor its value
exposed to the VMM.

> + /* Copy TDVMCALL result registers to output struct: */
> + movq %r11, TDVMCALL_r11(%r9)
> + movq %r12, TDVMCALL_r12(%r9)
> + movq %r13, TDVMCALL_r13(%r9)
> + movq %r14, TDVMCALL_r14(%r9)
> + movq %r15, TDVMCALL_r15(%r9)
> +1:
> + /*
> + * Zero out registers exposed to the VMM to avoid
> + * speculative execution with VMM-controlled values.
> + */
> + xor %r10d, %r10d
> + xor %r11d, %r11d
> + xor %r12d, %r12d
> + xor %r13d, %r13d
> + xor %r14d, %r14d
> + xor %r15d, %r15d
> +
> + /* Restore non-volatile GPRs that are exposed to the VMM. */
> + pop %r12
> + pop %r13
> + pop %r14
> + pop %r15
> +
> + FRAME_END
> + ret
> +2:
> + ud2
> +SYM_CODE_END(do_tdvmcall)
> +
> +/* Helper function for standard type of TDVMCALL */
> +SYM_FUNC_START(__tdvmcall)
> + /* Set TDVMCALL type info (0 - Standard, > 0 - vendor) in R10 */
> + xor %r10, %r10
> + call do_tdvmcall
> + retq
> +SYM_FUNC_END(__tdvmcall)

Why do we need this helper? Why does it need to be in assembly?

> diff --git a/arch/x86/kernel/tdx.c b/arch/x86/kernel/tdx.c
> index 6a7193fead08..29c52128b9c0 100644
> --- a/arch/x86/kernel/tdx.c
> +++ b/arch/x86/kernel/tdx.c
> @@ -1,8 +1,44 @@
> // SPDX-License-Identifier: GPL-2.0
> /* Copyright (C) 2020 Intel Corporation */
>
> +#define pr_fmt(fmt) "TDX: " fmt
> +
> #include <asm/tdx.h>
>
> +/*
> + * Wrapper for use case that checks for error code and print warning message.
> + */

This comment isn't very useful. I can see the error check and warning
by reading the code.

> +static inline u64 tdvmcall(u64 fn, u64 r12, u64 r13, u64 r14, u64 r15)
> +{
> + u64 err;
> +
> + err = __tdvmcall(fn, r12, r13, r14, r15, NULL);
> +
> + if (err)
> + pr_warn_ratelimited("TDVMCALL fn:%llx failed with err:%llx\n",
> + fn, err);
> +
> + return err;
> +}
> +
> +/*
> + * Wrapper for the semi-common case where we need single output value (R11).
> + */
> +static inline u64 tdvmcall_out_r11(u64 fn, u64 r12, u64 r13, u64 r14, u64 r15)
> +{
> +
> + struct tdvmcall_output out = {0};
> + u64 err;
> +
> + err = __tdvmcall(fn, r12, r13, r14, r15, &out);
> +
> + if (err)
> + pr_warn_ratelimited("TDVMCALL fn:%llx failed with err:%llx\n",
> + fn, err);
> +
> + return out.r11;
> +}

How do callers check for errors? Is the error value superfluously
returned in r11 and another output register?