From 3f6b834331b899ce5c0d8ad9dc2f95c30f76ce45 Mon Sep 17 00:00:00 2001 From: Dave Patel Date: Sun, 30 Aug 2026 10:33:01 +0100 Subject: [PATCH 1/3] =?UTF-8?q?xtest:=20riscv:=20Shared=20FP=E2=80=91conte?= =?UTF-8?q?xt=20test=20helpers=20for=20xtest=20and=20os=5Ftest=20TA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce riscv_fp_ctx.h, a small assembly-based helper library used by both xtest and the os_test TA to observe the behaviour of floating‑point registers across a call. The goal is to verify correct FP context preservation without interference from compiler optimisations. Only callee‑saved FP registers (fs0..fs11) and fcsr are covered. Caller‑saved register (ft0..ft11, fa0..fa7) are intentionally excluded since the ABI allows a callee to clobber them. The routines are implemented in assembly to ensure value remain in FP registers rather than being spilled to the stack by the compiler. They are emitted from a header instead of a .S file so both xtest and the TA can share the same code without adding assembler rules to their respective build systems. Signed-off-by: Dave Patel --- ta/os_test/include/riscv_fp_ctx.h | 243 ++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 ta/os_test/include/riscv_fp_ctx.h diff --git a/ta/os_test/include/riscv_fp_ctx.h b/ta/os_test/include/riscv_fp_ctx.h new file mode 100644 index 000000000..b86715f9a --- /dev/null +++ b/ta/os_test/include/riscv_fp_ctx.h @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Copyright (c) 2026, RISCStar Solutions Limited + */ + +#ifndef RISCV_FP_CTX_H +#define RISCV_FP_CTX_H + +#if defined(__riscv) && defined(__riscv_flen) && __riscv_flen == 64 +#define RISCV_FP_CTX_SUPPORTED 1 + +#include +#include + +/* + * The floating-point state the RISC-V calling convention requires a callee + * to preserve. Everything else, ft0..ft11 and fa0..fa7, may legitimately be + * clobbered by a call, so a caller cannot tell a context switching bug from + * a compiler doing what the ABI allows and those registers are left out of + * these checks. + * + * The layout is shared with riscv_fp_rv64.S. + */ +struct riscv_fp_ctx { + uint64_t fs[12]; /* fs0..fs11 */ + uint32_t fcsr; +}; + +/* + * Installs @in, calls fn(arg), stores what is left of the context in @out + * and returns what fn returned. The caller's own context is preserved. + */ +unsigned long riscv_fp_ctx_roundtrip(const struct riscv_fp_ctx *in, + struct riscv_fp_ctx *out, + unsigned long (*fn)(void *), void *arg); + +/* + * Installs @in and returns with it still in the registers. This breaks the + * calling convention on purpose, the caller must have no live + * floating-point values of its own. + */ +void riscv_fp_ctx_load(const struct riscv_fp_ctx *in); + +/* Stores the current context to @out without changing it */ +void riscv_fp_ctx_store(struct riscv_fp_ctx *out); + +/* + * Fills @ctx with a value per register that is distinct, derived from + * @seed, and a finite double rather than a NaN or an infinity so that + * nothing traps should a value reach an arithmetic instruction. fcsr gets a + * rounding mode and accrued exception flags that differ from the reset + * value, so that a save or restore which forgets fcsr is caught too. + */ +static inline void riscv_fp_ctx_pattern(struct riscv_fp_ctx *ctx, + uint32_t seed) +{ + size_t n = 0; + + for (n = 0; n < sizeof(ctx->fs) / sizeof(ctx->fs[0]); n++) + ctx->fs[n] = ((uint64_t)(0x3fd0 + n) << 48) | + ((uint64_t)seed << 16) | (n + 1); + + /* + * frm = 2, round down, which differs from the reset value, and all + * five accrued exception flags set. The flags are sticky and are + * only ever cleared explicitly, so starting from all ones keeps the + * comparison from tripping over a called function that happens to + * do some arithmetic of its own, while a save or restore that drops + * fcsr altogether still shows up. + */ + ctx->fcsr = (2 << 5) | 0x1f; +} + +/* + * Returns the index of the first field of @a that differs from @b, 12 if + * only fcsr differs, and -1 if the two contexts are identical. + */ +static inline int riscv_fp_ctx_diff(const struct riscv_fp_ctx *a, + const struct riscv_fp_ctx *b) +{ + size_t n = 0; + + for (n = 0; n < sizeof(a->fs) / sizeof(a->fs[0]); n++) + if (a->fs[n] != b->fs[n]) + return (int)n; + + if (a->fcsr != b->fcsr) + return 12; + + return -1; +} + +/* + * The three routines above are written in assembly because the whole point + * is to have the values actually sitting in fs0..fs11 across a call, and + * from C the compiler would be free to keep them on the stack instead, + * which would test nothing. + * + * They are emitted here, rather than from a .S file, so that the same + * implementation serves the TA and xtest without either build system + * needing to grow an assembler rule. Exactly one translation unit per + * binary must define RISCV_FP_CTX_IMPLEMENTATION before including this. + * + * Comments inside the block use '#' rather than C syntax: these strings do + * not go through the preprocessor the way a .S file does, so the assembler + * sees them verbatim, and Clang's integrated assembler rejects a comment + * in the C form. + * + * Stack frame of riscv_fp_ctx_roundtrip(): + * 0 ra + * 8 s0 + * 16 caller's fcsr + * 24 caller's fs0..fs11 + */ +#ifdef RISCV_FP_CTX_IMPLEMENTATION +_Static_assert(offsetof(struct riscv_fp_ctx, fs) == 0, + "riscv_fp_ctx layout out of sync with the assembly below"); +_Static_assert(offsetof(struct riscv_fp_ctx, fcsr) == 96, + "riscv_fp_ctx layout out of sync with the assembly below"); + +__asm__( +" .text\n" +"\n" +" .globl riscv_fp_ctx_roundtrip\n" +" .type riscv_fp_ctx_roundtrip, @function\n" +"riscv_fp_ctx_roundtrip:\n" +" addi sp, sp, -128\n" +" sd ra, 0(sp)\n" +" sd s0, 8(sp)\n" +" # Preserve the caller's own context before overwriting it\n" +" frcsr t0\n" +" sw t0, 16(sp)\n" +" fsd fs0, 24(sp)\n" +" fsd fs1, 32(sp)\n" +" fsd fs2, 40(sp)\n" +" fsd fs3, 48(sp)\n" +" fsd fs4, 56(sp)\n" +" fsd fs5, 64(sp)\n" +" fsd fs6, 72(sp)\n" +" fsd fs7, 80(sp)\n" +" fsd fs8, 88(sp)\n" +" fsd fs9, 96(sp)\n" +" fsd fs10, 104(sp)\n" +" fsd fs11, 112(sp)\n" +" mv s0, a1 # out\n" +" mv t1, a2 # fn\n" +" # Install the pattern taken from *in\n" +" fld fs0, 0(a0)\n" +" fld fs1, 8(a0)\n" +" fld fs2, 16(a0)\n" +" fld fs3, 24(a0)\n" +" fld fs4, 32(a0)\n" +" fld fs5, 40(a0)\n" +" fld fs6, 48(a0)\n" +" fld fs7, 56(a0)\n" +" fld fs8, 64(a0)\n" +" fld fs9, 72(a0)\n" +" fld fs10, 80(a0)\n" +" fld fs11, 88(a0)\n" +" lw t0, 96(a0)\n" +" fscsr t0\n" +" mv a0, a3 # arg\n" +" jalr t1\n" +" # Capture what survived, fn's return value stays in a0\n" +" fsd fs0, 0(s0)\n" +" fsd fs1, 8(s0)\n" +" fsd fs2, 16(s0)\n" +" fsd fs3, 24(s0)\n" +" fsd fs4, 32(s0)\n" +" fsd fs5, 40(s0)\n" +" fsd fs6, 48(s0)\n" +" fsd fs7, 56(s0)\n" +" fsd fs8, 64(s0)\n" +" fsd fs9, 72(s0)\n" +" fsd fs10, 80(s0)\n" +" fsd fs11, 88(s0)\n" +" frcsr t0\n" +" sw t0, 96(s0)\n" +" # Put the caller's own context back\n" +" fld fs0, 24(sp)\n" +" fld fs1, 32(sp)\n" +" fld fs2, 40(sp)\n" +" fld fs3, 48(sp)\n" +" fld fs4, 56(sp)\n" +" fld fs5, 64(sp)\n" +" fld fs6, 72(sp)\n" +" fld fs7, 80(sp)\n" +" fld fs8, 88(sp)\n" +" fld fs9, 96(sp)\n" +" fld fs10, 104(sp)\n" +" fld fs11, 112(sp)\n" +" lw t0, 16(sp)\n" +" fscsr t0\n" +" ld ra, 0(sp)\n" +" ld s0, 8(sp)\n" +" addi sp, sp, 128\n" +" ret\n" +" .size riscv_fp_ctx_roundtrip, .-riscv_fp_ctx_roundtrip\n" +"\n" +" .globl riscv_fp_ctx_load\n" +" .type riscv_fp_ctx_load, @function\n" +"riscv_fp_ctx_load:\n" +" fld fs0, 0(a0)\n" +" fld fs1, 8(a0)\n" +" fld fs2, 16(a0)\n" +" fld fs3, 24(a0)\n" +" fld fs4, 32(a0)\n" +" fld fs5, 40(a0)\n" +" fld fs6, 48(a0)\n" +" fld fs7, 56(a0)\n" +" fld fs8, 64(a0)\n" +" fld fs9, 72(a0)\n" +" fld fs10, 80(a0)\n" +" fld fs11, 88(a0)\n" +" lw t0, 96(a0)\n" +" fscsr t0\n" +" ret\n" +" .size riscv_fp_ctx_load, .-riscv_fp_ctx_load\n" +"\n" +" .globl riscv_fp_ctx_store\n" +" .type riscv_fp_ctx_store, @function\n" +"riscv_fp_ctx_store:\n" +" fsd fs0, 0(a0)\n" +" fsd fs1, 8(a0)\n" +" fsd fs2, 16(a0)\n" +" fsd fs3, 24(a0)\n" +" fsd fs4, 32(a0)\n" +" fsd fs5, 40(a0)\n" +" fsd fs6, 48(a0)\n" +" fsd fs7, 56(a0)\n" +" fsd fs8, 64(a0)\n" +" fsd fs9, 72(a0)\n" +" fsd fs10, 80(a0)\n" +" fsd fs11, 88(a0)\n" +" frcsr t0\n" +" sw t0, 96(a0)\n" +" ret\n" +" .size riscv_fp_ctx_store, .-riscv_fp_ctx_store\n" +); +#endif /* RISCV_FP_CTX_IMPLEMENTATION */ + +#endif /* __riscv && __riscv_flen == 64 */ +#endif /* RISCV_FP_CTX_H */ From bfdf36371d4e269d560b0f7e23810722b25f550a Mon Sep 17 00:00:00 2001 From: Dave Patel Date: Sun, 30 Aug 2026 10:33:01 +0100 Subject: [PATCH 2/3] ta: os_test: riscv: check FP context switching from a TA Add TA_OS_TEST_CMD_RISCV_FP_CONTEXT, which runs a set of sub-tests that each install a known floating-point context, make a different kind of excursion out of the TA, and check that the context comes back intact. A TA runs with the floating-point unit disabled and is handed a context on the first floating-point instruction it executes, so every sub-test starts by taking that trap. What they then cover is: SYSCALL a syscall serviced entirely inside the TEE, where the TA context is saved on the way in and has to be given back on the first floating-point instruction after the return RPC TEE_Wait(), which suspends the thread, runs the normal world and resumes through thread_resume_from_rpc() CRYPTO a digest, which the TEE may compute in a secure kernel floating-point section of its own and so has to take the registers from the TA and hand them back TAINT leaves a pattern in the registers and returns CHECK_TAINT reads the registers before writing any of them and fails if an earlier TA's pattern is still there On failure the index of the first field that did not survive is reported back to the caller so that a failure says which register was lost. Signed-off-by: Dave Patel --- ta/os_test/include/os_test.h | 2 + ta/os_test/include/ta_os_test.h | 22 ++++ ta/os_test/os_test.c | 184 ++++++++++++++++++++++++++++++++ ta/os_test/ta_entry.c | 3 + 4 files changed, 211 insertions(+) diff --git a/ta/os_test/include/os_test.h b/ta/os_test/include/os_test.h index cddcc604d..5f7cbdcb3 100644 --- a/ta/os_test/include/os_test.h +++ b/ta/os_test/include/os_test.h @@ -51,5 +51,7 @@ TEE_Result ta_entry_asan_global(void); TEE_Result ta_entry_asan_malloc(void); TEE_Result ta_entry_asan_memfunc(void); TEE_Result ta_entry_asan_uaf(void); +TEE_Result ta_entry_riscv_fp_context(uint32_t param_types, + TEE_Param params[4]); #endif /*OS_TEST_H */ diff --git a/ta/os_test/include/ta_os_test.h b/ta/os_test/include/ta_os_test.h index a6b71a699..8b523c3bc 100644 --- a/ta/os_test/include/ta_os_test.h +++ b/ta/os_test/include/ta_os_test.h @@ -52,5 +52,27 @@ #define TA_OS_TEST_CMD_ASAN_MALLOC 40 #define TA_OS_TEST_CMD_ASAN_UAF 41 #define TA_OS_TEST_CMD_ASAN_MEMFUNC 42 +#define TA_OS_TEST_CMD_RISCV_FP_CONTEXT 43 + +/* + * Sub-tests of TA_OS_TEST_CMD_RISCV_FP_CONTEXT, selected with + * params[0].value.a. params[0].value.b carries a seed which picks the + * register pattern, so that one invocation can be told apart from another. + * On failure params[1].value.a holds the index of the first field that did + * not survive, 0..11 for fs0..fs11 and 12 for fcsr. + */ + +/* Return immediately without touching the floating-point unit */ +#define TA_RISCV_FP_SUBTEST_NO_FP 0 +/* Check the context survives a syscall that stays inside the TEE */ +#define TA_RISCV_FP_SUBTEST_SYSCALL 1 +/* Check the context survives an RPC out to the normal world */ +#define TA_RISCV_FP_SUBTEST_RPC 2 +/* Check the context survives a crypto operation carried out by the TEE */ +#define TA_RISCV_FP_SUBTEST_CRYPTO 3 +/* Leave the pattern in the registers and return */ +#define TA_RISCV_FP_SUBTEST_TAINT 4 +/* Fail if the registers still hold the pattern left by an earlier TA */ +#define TA_RISCV_FP_SUBTEST_CHECK_TAINT 5 #endif /*TA_OS_TEST_H */ diff --git a/ta/os_test/os_test.c b/ta/os_test/os_test.c index 83cf692e8..a2055eb15 100644 --- a/ta/os_test/os_test.c +++ b/ta/os_test/os_test.c @@ -16,6 +16,9 @@ #include #include +#define RISCV_FP_CTX_IMPLEMENTATION +#include + #include "os_test.h" #include "test_float_subj.h" #include "os_test_lib.h" @@ -1721,3 +1724,184 @@ TEE_Result ta_entry_asan_uaf(void) return TEE_ERROR_NOT_SUPPORTED; } #endif +#ifdef RISCV_FP_CTX_SUPPORTED +/* + * Floating-point context switching. + * + * A TA runs with the floating-point unit disabled and is given it on the + * first floating-point instruction it executes, so every one of these + * sub-tests starts by trapping into the TEE and being handed a context. + * What each one then checks is that the context comes back intact across a + * different kind of excursion out of the TA. + */ + +static unsigned long fp_call_syscall(void *arg __unused) +{ + TEE_Time t = { }; + + /* + * A syscall handled entirely inside the TEE. The TA's context is + * saved on the way in and has to be handed back on the first + * floating-point instruction after the return. + */ + TEE_GetSystemTime(&t); + + return TEE_SUCCESS; +} + +static unsigned long fp_call_rpc(void *arg) +{ + uint32_t *ms = arg; + + /* + * TEE_Wait() leaves the TEE altogether: the thread is suspended, + * the normal world runs, and the thread is later resumed through + * thread_resume_from_rpc(). Both the TA context and the normal + * world context have to survive that. + */ + return TEE_Wait(*ms); +} + +static unsigned long fp_call_crypto(void *arg __unused) +{ + TEE_OperationHandle op = TEE_HANDLE_NULL; + uint8_t digest[32] = { }; + size_t digest_len = sizeof(digest); + static const uint8_t msg[] = "floating-point context switch"; + TEE_Result res = TEE_ERROR_GENERIC; + + /* + * Crypto work is done by the TEE on the TA's behalf and may open a + * secure kernel floating-point section of its own, which has to + * take the registers from the TA and give them back. + */ + res = TEE_AllocateOperation(&op, TEE_ALG_SHA256, TEE_MODE_DIGEST, 0); + if (res) + return res; + + res = TEE_DigestDoFinal(op, msg, sizeof(msg), digest, &digest_len); + + TEE_FreeOperation(op); + + return res; +} + +static TEE_Result fp_check_roundtrip(uint32_t seed, TEE_Param params[4], + unsigned long (*fn)(void *), void *arg) +{ + struct riscv_fp_ctx expect = { }; + struct riscv_fp_ctx got = { }; + unsigned long res = 0; + int diff = 0; + + riscv_fp_ctx_pattern(&expect, seed); + + res = riscv_fp_ctx_roundtrip(&expect, &got, fn, arg); + if (res != TEE_SUCCESS) { + EMSG("FP context: call failed: %#lx", res); + return (TEE_Result)res; + } + + diff = riscv_fp_ctx_diff(&expect, &got); + if (diff < 0) + return TEE_SUCCESS; + + params[1].value.a = diff; + if (diff == 12) + EMSG("FP context: fcsr changed, expected %#" PRIx32 + " got %#" PRIx32, expect.fcsr, got.fcsr); + else + EMSG("FP context: fs%d changed, expected %#" PRIx64 + " got %#" PRIx64, diff, expect.fs[diff], got.fs[diff]); + + return TEE_ERROR_GENERIC; +} + +TEE_Result ta_entry_riscv_fp_context(uint32_t param_types, + TEE_Param params[4]) +{ + uint32_t wait_ms = 10; + uint32_t subtest = 0; + uint32_t seed = 0; + + if (param_types != TEE_PARAM_TYPES(TEE_PARAM_TYPE_VALUE_INPUT, + TEE_PARAM_TYPE_VALUE_OUTPUT, + TEE_PARAM_TYPE_NONE, + TEE_PARAM_TYPE_NONE)) + return TEE_ERROR_BAD_PARAMETERS; + + subtest = params[0].value.a; + seed = params[0].value.b; + params[1].value.a = 0; + params[1].value.b = 0; + + switch (subtest) { + case TA_RISCV_FP_SUBTEST_NO_FP: + /* + * Used by the normal world to check that its own context + * survives a call into a TA which never enables the + * floating-point unit, the path where the TEE elides the + * restore on the way out. + */ + return TEE_SUCCESS; + + case TA_RISCV_FP_SUBTEST_SYSCALL: + return fp_check_roundtrip(seed, params, fp_call_syscall, NULL); + + case TA_RISCV_FP_SUBTEST_RPC: + return fp_check_roundtrip(seed, params, fp_call_rpc, &wait_ms); + + case TA_RISCV_FP_SUBTEST_CRYPTO: + return fp_check_roundtrip(seed, params, fp_call_crypto, NULL); + + case TA_RISCV_FP_SUBTEST_TAINT: { + struct riscv_fp_ctx taint = { }; + + riscv_fp_ctx_pattern(&taint, seed); + riscv_fp_ctx_load(&taint); + + return TEE_SUCCESS; + } + + case TA_RISCV_FP_SUBTEST_CHECK_TAINT: { + struct riscv_fp_ctx taint = { }; + struct riscv_fp_ctx got = { }; + size_t n = 0; + + /* + * Read the registers before writing any of them, so that + * the first floating-point instruction this TA executes is + * the one below. Whatever an earlier TA left behind must + * not be visible here. + * + * Nothing is asserted about what the registers do hold: on + * the way back to the normal world the TEE puts the normal + * world context back, so by the time this TA runs they + * carry that rather than a known constant. + */ + riscv_fp_ctx_store(&got); + riscv_fp_ctx_pattern(&taint, seed); + + for (n = 0; n < ARRAY_SIZE(got.fs); n++) { + if (got.fs[n] == taint.fs[n]) { + EMSG("FP context: fs%zu still holds %#" PRIx64 + " left by an earlier TA", n, taint.fs[n]); + params[1].value.a = n; + return TEE_ERROR_GENERIC; + } + } + + return TEE_SUCCESS; + } + + default: + return TEE_ERROR_BAD_PARAMETERS; + } +} +#else /*RISCV_FP_CTX_SUPPORTED*/ +TEE_Result ta_entry_riscv_fp_context(uint32_t param_types __unused, + TEE_Param params[4] __unused) +{ + return TEE_ERROR_NOT_SUPPORTED; +} +#endif /*RISCV_FP_CTX_SUPPORTED*/ diff --git a/ta/os_test/ta_entry.c b/ta/os_test/ta_entry.c index 069e39ac8..f858dd3d5 100644 --- a/ta/os_test/ta_entry.c +++ b/ta/os_test/ta_entry.c @@ -191,6 +191,9 @@ TEE_Result TA_InvokeCommandEntryPoint(void *pSessionContext, case TA_OS_TEST_CMD_ASAN_MEMFUNC: return ta_entry_asan_memfunc(); + case TA_OS_TEST_CMD_RISCV_FP_CONTEXT: + return ta_entry_riscv_fp_context(nParamTypes, pParams); + default: return TEE_ERROR_BAD_PARAMETERS; } From 8c0931758e30af8a7ecfdd1f635e8d902b1dca6d Mon Sep 17 00:00:00 2001 From: Dave Patel Date: Sun, 30 Aug 2026 10:33:01 +0100 Subject: [PATCH 3/3] xtest: regression 1045: RISC-V floating-point context switching Drive the os_test sub-tests from xtest and add the checks that can only be made from the normal world. Alongside the TA side cases the test holds a pattern in this process' fs0..fs11 and fcsr across a call into the TEE. That is the normal world half of the domain switch, and it is run twice: once against a TA command that uses floating point and once against one that does not, since the TEE restores the normal world context in the first case and skips the restore in the second, having never disturbed the registers. Both have to leave the caller's registers exactly as it left them. A concurrency case runs the RPC sub-test from several threads at once, since the floating-point bookkeeping is per OP-TEE thread and one thread's context must not surface in another. The test skips itself when the TA reports TEE_ERROR_NOT_SUPPORTED, which covers both a non-RISC-V target and an OP-TEE built without CFG_WITH_VFP. Signed-off-by: Dave Patel --- host/xtest/regression_1000.c | 251 +++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/host/xtest/regression_1000.c b/host/xtest/regression_1000.c index 18b15b516..576161136 100644 --- a/host/xtest/regression_1000.c +++ b/host/xtest/regression_1000.c @@ -13,6 +13,9 @@ #include #include #endif +#define RISCV_FP_CTX_IMPLEMENTATION +#include + #include #include #include @@ -3475,3 +3478,251 @@ static void xtest_tee_test_1042(ADBG_Case_t *c) } ADBG_CASE_DEFINE(regression, 1042, xtest_tee_test_1042, "Test ASAN (Memory address sanitizer)"); + +/* + * RISC-V floating-point context switching. + * + * A TA runs with the floating-point unit disabled and is handed a context + * on the first floating-point instruction it executes, while the normal + * world context is switched eagerly whenever a thread crosses into the TEE + * and back. The sub-tests below check both halves of that: the TA side by + * asking the TA to verify its own registers across various excursions, and + * the normal world side by holding a pattern in this process' registers + * across a call into the TEE. + */ + +static TEEC_Result fp_invoke(TEEC_Session *session, uint32_t subtest, + uint32_t seed, uint32_t *bad_field, + uint32_t *ret_orig) +{ + TEEC_Operation op = { }; + TEEC_Result res = TEEC_ERROR_GENERIC; + + op.paramTypes = TEEC_PARAM_TYPES(TEEC_VALUE_INPUT, TEEC_VALUE_OUTPUT, + TEEC_NONE, TEEC_NONE); + op.params[0].value.a = subtest; + op.params[0].value.b = seed; + + res = TEEC_InvokeCommand(session, TA_OS_TEST_CMD_RISCV_FP_CONTEXT, + &op, ret_orig); + if (bad_field) + *bad_field = op.params[1].value.a; + + return res; +} + +static void fp_log_bad_field(uint32_t field) +{ + if (field == 12) + Do_ADBG_Log(" fcsr was not preserved"); + else + Do_ADBG_Log(" fs%u was the first register not preserved", + field); +} + +#ifdef RISCV_FP_CTX_SUPPORTED +struct fp_ree_arg { + TEEC_Session *session; + uint32_t subtest; + uint32_t seed; + uint32_t bad_field; + uint32_t ret_orig; +}; + +static unsigned long fp_ree_invoke(void *a) +{ + struct fp_ree_arg *arg = a; + + return fp_invoke(arg->session, arg->subtest, arg->seed, + &arg->bad_field, &arg->ret_orig); +} + +/* + * Holds a pattern in this process' fs0..fs11 and fcsr across an invoke and + * checks that the TEE gave them back. This is the normal world half of the + * domain switch: whatever the TEE does with the floating-point registers + * must not be visible here. + */ +static void fp_check_ree_preserved(ADBG_Case_t *c, TEEC_Session *session, + uint32_t subtest, uint32_t seed) +{ + struct riscv_fp_ctx expect = { }; + struct riscv_fp_ctx got = { }; + struct fp_ree_arg arg = { }; + TEEC_Result res = TEEC_ERROR_GENERIC; + int diff = 0; + + arg.session = session; + arg.subtest = subtest; + arg.seed = seed; + + riscv_fp_ctx_pattern(&expect, seed); + + res = (TEEC_Result)riscv_fp_ctx_roundtrip(&expect, &got, + fp_ree_invoke, &arg); + if (!ADBG_EXPECT_TEEC_SUCCESS(c, res)) { + if (arg.bad_field) + fp_log_bad_field(arg.bad_field); + return; + } + + diff = riscv_fp_ctx_diff(&expect, &got); + if (!ADBG_EXPECT_COMPARE_SIGNED(c, diff, ==, -1)) + fp_log_bad_field((uint32_t)diff); +} +#endif /*RISCV_FP_CTX_SUPPORTED*/ + +static void fp_check_ta_subtest(ADBG_Case_t *c, TEEC_Session *session, + uint32_t subtest, uint32_t seed) +{ + uint32_t ret_orig = 0; + uint32_t bad_field = 0; + TEEC_Result res = TEEC_ERROR_GENERIC; + + res = fp_invoke(session, subtest, seed, &bad_field, &ret_orig); + if (!ADBG_EXPECT_TEEC_SUCCESS(c, res)) + fp_log_bad_field(bad_field); +} + +struct test_1045_thread_arg { + pthread_t thr; + uint32_t seed; + TEEC_Result res; + uint32_t bad_field; +}; + +static void *test_1045_thread(void *a) +{ + struct test_1045_thread_arg *arg = a; + TEEC_Session session = { }; + uint32_t ret_orig = 0; + size_t n = 0; + + arg->res = xtest_teec_open_session(&session, &os_test_ta_uuid, NULL, + &ret_orig); + if (arg->res != TEEC_SUCCESS) + return NULL; + + for (n = 0; n < 8; n++) { + arg->res = fp_invoke(&session, TA_RISCV_FP_SUBTEST_RPC, + arg->seed, &arg->bad_field, &ret_orig); + if (arg->res != TEEC_SUCCESS) + break; + } + + TEEC_CloseSession(&session); + + return NULL; +} + +static void xtest_tee_test_1045(ADBG_Case_t *c) +{ + struct test_1045_thread_arg arg[NUM_THREADS] = { }; + TEEC_Session session = { }; + TEEC_Session tainted = { }; + uint32_t ret_orig = 0; + uint32_t bad_field = 0; + TEEC_Result res = TEEC_ERROR_GENERIC; + size_t nt = NUM_THREADS; + size_t n = 0; + + if (!ADBG_EXPECT_TEEC_SUCCESS(c, + xtest_teec_open_session(&session, &os_test_ta_uuid, + NULL, &ret_orig))) + return; + + /* + * Probe first: a TA built for an architecture without this test, or + * an OP-TEE built without floating-point context switching, has + * nothing to say here. + */ + res = fp_invoke(&session, TA_RISCV_FP_SUBTEST_SYSCALL, 0x11, + &bad_field, &ret_orig); + if (res == TEEC_ERROR_NOT_SUPPORTED) { + Do_ADBG_Log("TA has no RISC-V FP context test - skip tests"); + goto out; + } + + Do_ADBG_BeginSubCase(c, "TA context across a syscall"); + if (!ADBG_EXPECT_TEEC_SUCCESS(c, res)) + fp_log_bad_field(bad_field); + Do_ADBG_EndSubCase(c, "TA context across a syscall"); + + Do_ADBG_BeginSubCase(c, "TA context across an RPC"); + fp_check_ta_subtest(c, &session, TA_RISCV_FP_SUBTEST_RPC, 0x22); + Do_ADBG_EndSubCase(c, "TA context across an RPC"); + + Do_ADBG_BeginSubCase(c, "TA context across a crypto operation"); + fp_check_ta_subtest(c, &session, TA_RISCV_FP_SUBTEST_CRYPTO, 0x33); + Do_ADBG_EndSubCase(c, "TA context across a crypto operation"); + + /* + * A TA that has just been loaded must not find another TA's values + * in the registers. os_test is multi instance, so closing the + * session below unloads the instance that left the pattern behind + * and the next session gets a fresh floating-point context. + */ + Do_ADBG_BeginSubCase(c, "TA context is not carried between instances"); + res = xtest_teec_open_session(&tainted, &os_test_ta_uuid, NULL, + &ret_orig); + if (ADBG_EXPECT_TEEC_SUCCESS(c, res)) { + ADBG_EXPECT_TEEC_SUCCESS(c, + fp_invoke(&tainted, TA_RISCV_FP_SUBTEST_TAINT, 0x44, + NULL, &ret_orig)); + TEEC_CloseSession(&tainted); + + memset(&tainted, 0, sizeof(tainted)); + res = xtest_teec_open_session(&tainted, &os_test_ta_uuid, NULL, + &ret_orig); + if (ADBG_EXPECT_TEEC_SUCCESS(c, res)) { + fp_check_ta_subtest(c, &tainted, + TA_RISCV_FP_SUBTEST_CHECK_TAINT, + 0x44); + TEEC_CloseSession(&tainted); + } + } + Do_ADBG_EndSubCase(c, "TA context is not carried between instances"); + +#ifdef RISCV_FP_CTX_SUPPORTED + /* + * The TEE saves the normal world context on the way in. On the way + * out it puts it back, unless nothing in the TEE ever enabled the + * floating-point unit, in which case the registers were never + * disturbed and the restore is skipped. Both paths have to leave + * this process' registers exactly as it left them, so cover them + * with a TA command that uses floating point and one that does not. + */ + Do_ADBG_BeginSubCase(c, "REE context across an invoke using FP"); + fp_check_ree_preserved(c, &session, TA_RISCV_FP_SUBTEST_RPC, 0x55); + Do_ADBG_EndSubCase(c, "REE context across an invoke using FP"); + + Do_ADBG_BeginSubCase(c, "REE context across an invoke not using FP"); + fp_check_ree_preserved(c, &session, TA_RISCV_FP_SUBTEST_NO_FP, 0x66); + Do_ADBG_EndSubCase(c, "REE context across an invoke not using FP"); +#endif /*RISCV_FP_CTX_SUPPORTED*/ + + /* + * Each OP-TEE thread carries its own floating-point bookkeeping, so + * run the RPC case from several threads at once to check that one + * thread's context does not end up in another. + */ + Do_ADBG_BeginSubCase(c, "Concurrent TA contexts"); + for (n = 0; n < nt; n++) { + arg[n].seed = 0x80 + n; + if (!ADBG_EXPECT(c, 0, pthread_create(&arg[n].thr, NULL, + test_1045_thread, + arg + n))) + nt = n; /* break loop and start cleanup */ + } + for (n = 0; n < nt; n++) { + ADBG_EXPECT(c, 0, pthread_join(arg[n].thr, NULL)); + if (!ADBG_EXPECT_TEEC_SUCCESS(c, arg[n].res)) + fp_log_bad_field(arg[n].bad_field); + } + Do_ADBG_EndSubCase(c, "Concurrent TA contexts"); + +out: + TEEC_CloseSession(&session); +} +ADBG_CASE_DEFINE(regression, 1045, xtest_tee_test_1045, + "Test RISC-V floating-point context switching");