From d69cd0d88595cd1e6d979d5b0d0c89472cc22a90 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Fri, 23 Oct 2026 12:21:39 +0300 Subject: [PATCH 1/4] userspace: add kernel-only registry of SOF syscall callers Zephyr lets any user thread invoke any system call. SOF user threads have different trust levels: the LL scheduler and IPC user threads are SOF infrastructure, while DP threads run module code that can come from a third party. Add a registry in kernel-only memory that records the LL and IPC user thread of each core, and sof_syscall_caller_allowed() for validators to check the calling thread against a set of roles. Register the threads where they are created, before they start, and clear the LL entry before the thread object is freed. The registry is enabled with the new SOF_USERSPACE_SYSCALL_CALLER_CHECK option, default y with SOF_USERSPACE_LL. No validator uses it yet. Signed-off-by: Kai Vehmanen --- src/include/sof/userspace/syscall_caller.h | 73 ++++++++++++++++++++++ src/ipc/ipc-common.c | 2 + src/schedule/zephyr_domain.c | 26 +++++--- zephyr/CMakeLists.txt | 1 + zephyr/Kconfig | 12 ++++ zephyr/syscall/caller.c | 66 +++++++++++++++++++ 6 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 src/include/sof/userspace/syscall_caller.h create mode 100644 zephyr/syscall/caller.c diff --git a/src/include/sof/userspace/syscall_caller.h b/src/include/sof/userspace/syscall_caller.h new file mode 100644 index 000000000000..f0e1a4d1b1b4 --- /dev/null +++ b/src/include/sof/userspace/syscall_caller.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + */ + +/** + * \file + * \brief Restrict SOF system calls to the user threads that need them. + * + * Zephyr lets any user thread invoke any system call. SOF user threads have + * different trust levels: the LL scheduler thread and the IPC user thread + * are SOF infrastructure, while DP threads run module code that can come + * from a third party. Most SOF system calls are only needed by the + * infrastructure threads, so their validators check the caller here. + * + * The thread registry is kept in kernel-only memory. Do not derive caller + * identity from data reachable by user threads, such as ipc_user->thread[] + * or the LL domain objects, as a user thread could rewrite it. + */ + +#ifndef __SOF_USERSPACE_SYSCALL_CALLER_H__ +#define __SOF_USERSPACE_SYSCALL_CALLER_H__ + +#include + +struct k_thread; + +/** LL scheduler user thread (one per core) */ +#define SOF_SYSUSER_LL (1U << 0) +/** IPC user thread (one per core) */ +#define SOF_SYSUSER_IPC (1U << 1) +/** Either SOF infrastructure user thread */ +#define SOF_SYSUSER_INFRA (SOF_SYSUSER_LL | SOF_SYSUSER_IPC) + +#ifdef CONFIG_SOF_USERSPACE_SYSCALL_CALLER_CHECK + +/** + * Record @thread as the user thread with @role on @core. + * + * Kernel context only. Call before the thread is started, and call again + * with @thread NULL before the thread object is freed. + * + * @param role SOF_SYSUSER_LL or SOF_SYSUSER_IPC (exactly one bit) + * @param core DSP core the thread is pinned to + * @param thread the k_thread to register, or NULL to clear the entry + */ +void sof_sysuser_thread_set(unsigned int role, unsigned int core, struct k_thread *thread); + +/** + * Check whether the current thread holds one of @roles. + * + * For use in z_vrfy_*() validators. + * + * @param roles bitmask of SOF_SYSUSER_* roles that are allowed + * @return true if the calling thread is registered with an allowed role + */ +bool sof_syscall_caller_allowed(unsigned int roles); + +/** Kill the calling user thread unless it holds one of @roles. */ +#define SOF_SYSCALL_CALLER_CHECK(roles) K_OOPS(!sof_syscall_caller_allowed(roles)) + +#else + +static inline void sof_sysuser_thread_set(unsigned int role, unsigned int core, + struct k_thread *thread) +{ +} + +#define SOF_SYSCALL_CALLER_CHECK(roles) do { } while (0) + +#endif /* CONFIG_SOF_USERSPACE_SYSCALL_CALLER_CHECK */ + +#endif /* __SOF_USERSPACE_SYSCALL_CALLER_H__ */ diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index bbc4a9b8a7d3..dd8443c50067 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -47,6 +47,7 @@ #endif #include +#include LOG_MODULE_REGISTER(ipc, CONFIG_SOF_LOG_LEVEL); @@ -592,6 +593,7 @@ __cold static int ipc_user_init_thread(struct ipc_user *ipc_user, unsigned int c ipc_user_kevent[core] = event; ipc_user_kthread[core] = thread; + sof_sysuser_thread_set(SOF_SYSUSER_IPC, core, thread); /* for ipc_user_thread_fn() and ipc_thread_user() in user context */ ipc_user->event[core] = event; diff --git a/src/schedule/zephyr_domain.c b/src/schedule/zephyr_domain.c index 5ceee36fa98a..121aab7e24df 100644 --- a/src/schedule/zephyr_domain.c +++ b/src/schedule/zephyr_domain.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -337,22 +338,28 @@ static int zephyr_domain_thread_init(struct ll_schedule_domain *domain, thread_name[sizeof(thread_name) - 2] = '0' + core; - /* Allocate thread structure dynamically */ - dt->ll_thread = k_object_alloc(K_OBJ_THREAD); - if (!dt->ll_thread) { + /* + * Allocate thread structure dynamically. Keep the handle in a local: + * the domain is user-writable, so dt->ll_thread must not be read back + * for the kernel-only tables below. + */ + thread = k_object_alloc(K_OBJ_THREAD); + if (!thread) { tr_err(&ll_tr, "Failed to allocate thread object for core %d", core); k_object_free(dt->sem); dt->sem = NULL; return -ENOMEM; } - thread = k_thread_create(dt->ll_thread, ll_sched_stack[core], ZEPHYR_LL_STACK_SIZE, - zephyr_domain_thread_fn, zephyr_domain, - INT_TO_POINTER(core), NULL, CONFIG_LL_THREAD_PRIORITY, - K_USER, K_FOREVER); + k_thread_create(thread, ll_sched_stack[core], ZEPHYR_LL_STACK_SIZE, + zephyr_domain_thread_fn, zephyr_domain, + INT_TO_POINTER(core), NULL, CONFIG_LL_THREAD_PRIORITY, + K_USER, K_FOREVER); /* record in the kernel-only table for syscall-context lookups */ - ll_thread_tid[core] = dt->ll_thread; + ll_thread_tid[core] = thread; + sof_sysuser_thread_set(SOF_SYSUSER_LL, core, thread); + dt->ll_thread = thread; #ifdef CONFIG_SCHED_CPU_MASK k_thread_cpu_pin(thread, core); @@ -485,10 +492,11 @@ static void zephyr_domain_thread_free(struct ll_schedule_domain *domain, k_mutex_unlock(domain->lock); if (dt->ll_thread) { + sof_sysuser_thread_set(SOF_SYSUSER_LL, core, NULL); + ll_thread_tid[core] = NULL; k_thread_abort(dt->ll_thread); k_object_free(dt->ll_thread); dt->ll_thread = NULL; - ll_thread_tid[core] = NULL; } if (dt->sem) { diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index f471aea8e0b7..435a1e26f686 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -614,6 +614,7 @@ zephyr_library_sources_ifdef(CONFIG_SHELL zephyr_syscall_header(include/sof/lib/cpu.h) zephyr_library_sources_ifdef(CONFIG_USERSPACE syscall/cpu.c) +zephyr_library_sources_ifdef(CONFIG_SOF_USERSPACE_SYSCALL_CALLER_CHECK syscall/caller.c) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/audio/module_adapter/module/generic.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/audio/kpb.h) zephyr_syscall_header(${SOF_SRC_PATH}/include/sof/lib/fast-get.h) diff --git a/zephyr/Kconfig b/zephyr/Kconfig index 867100623b42..88f594f8d787 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -56,6 +56,18 @@ config SOF_USERSPACE_LL audio application threads. If unsure, select "N". +config SOF_USERSPACE_SYSCALL_CALLER_CHECK + bool "Restrict SOF system calls to the threads that need them" + depends on SOF_USERSPACE_LL + default y + help + Zephyr allows any user thread to invoke any system call. With + this option, the validators of SOF system calls that are only + needed by the LL scheduler and IPC user threads kill any other + calling user thread, such as a DP module thread. System calls + needed to implement the module interface stay available to all + user threads. Disable only to debug a suspected false positive. + config SOF_ZEPHYR_HEAP_CACHED bool "Cached Zephyr heap for SOF memory non-shared zones" default y if CAVS || ACE diff --git a/zephyr/syscall/caller.c b/zephyr/syscall/caller.c new file mode 100644 index 000000000000..ccb9746da299 --- /dev/null +++ b/zephyr/syscall/caller.c @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. + +#include +#include +#include +#include + +#define SYSUSER_ROLE_COUNT 2 + +/* + * Kernel-only table of SOF infrastructure user threads. This must not carry + * an APP_SYSUSER_* or APP_TASK_* partition marker: if a user thread could + * write it, it could register itself and pass every caller check. + */ +static atomic_ptr_t sysuser_threads[SYSUSER_ROLE_COUNT][CONFIG_CORE_COUNT]; + +static int sysuser_role_index(unsigned int role) +{ + switch (role) { + case SOF_SYSUSER_LL: + return 0; + case SOF_SYSUSER_IPC: + return 1; + default: + return -1; + } +} + +void sof_sysuser_thread_set(unsigned int role, unsigned int core, struct k_thread *thread) +{ + int idx = sysuser_role_index(role); + + __ASSERT(!k_is_user_context(), "kernel context only"); + + if (idx < 0 || core >= CONFIG_CORE_COUNT) { + __ASSERT(false, "invalid role %u or core %u", role, core); + return; + } + + atomic_ptr_set(&sysuser_threads[idx][core], thread); +} + +bool sof_syscall_caller_allowed(unsigned int roles) +{ + struct k_thread *self = k_current_get(); + unsigned int role, core; + int idx; + + /* + * Check all cores rather than the current one: it costs a few + * compares and does not depend on the caller staying pinned. + */ + for (role = SOF_SYSUSER_LL; role & SOF_SYSUSER_INFRA; role <<= 1) { + if (!(roles & role)) + continue; + + idx = sysuser_role_index(role); + for (core = 0; core < CONFIG_CORE_COUNT; core++) + if (atomic_ptr_get(&sysuser_threads[idx][core]) == self) + return true; + } + + return false; +} From 5762d9207ee024ec746722ed766d59dac851c738 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 21 Oct 2026 16:17:09 +0300 Subject: [PATCH 2/4] userspace: restrict SOF syscalls to the threads that need them Use the caller registry in the validators of SOF system calls, so DP module threads can only reach the syscalls needed to implement the module interface, including sending notifications: - any user thread: cpu_get_id, the mod_* module syscalls, ipc_msg_send, ipc_msg_list_remove and send_resource_notif - IPC user thread only: ipc_compound_pre_start, ipc_compound_post_start, ipc_wait_for_compound_msg, lib_manager_free_module - LL user thread only: scheduler_dp_ll_tick - LL or IPC user thread: all other SOF syscalls DP modules send notifications with mod_ipc_msg_init() and ipc_msg_send(), free them with mod_ipc_msg_free() from their .free() callback, and dp_task_run() reports processing errors with send_resource_notif(), so these stay open to all user threads. lib_manager_free_module() previously identified the IPC thread through ipc_user->thread[], which user threads can write. It now uses the registry, keeping the old check only if the option is disabled. The HDA DMA and SSP DAI userspace tests call DMA syscalls from their own user thread, so register that thread for the duration of the test. Signed-off-by: Kai Vehmanen --- src/audio/kpb.c | 3 +++ src/debug/debug_stream/debug_stream_slot.c | 3 +++ src/ipc/ipc-common.c | 2 ++ src/ipc/ipc4/handler-kernel.c | 9 +++++++ src/ipc/ipc4/notification.c | 6 +++++ src/library_manager/lib_manager.c | 9 +++++++ src/schedule/zephyr_dp_schedule.c | 3 +++ src/schedule/zephyr_dp_schedule_application.c | 3 +++ src/schedule/zephyr_ll.c | 5 ++++ zephyr/syscall/alloc.c | 5 ++++ zephyr/syscall/dai.c | 3 +++ zephyr/syscall/sof_dma.c | 25 +++++++++++++++++++ zephyr/syscall/vregion.c | 15 +++++++++++ zephyr/test/userspace/test_intel_hda_dma.c | 7 ++++++ zephyr/test/userspace/test_intel_ssp_dai.c | 7 ++++++ 15 files changed, 105 insertions(+) diff --git a/src/audio/kpb.c b/src/audio/kpb.c index a2739b14e821..ec0104bf671c 100644 --- a/src/audio/kpb.c +++ b/src/audio/kpb.c @@ -503,8 +503,11 @@ void z_impl_kpb_notifier_init(struct comp_dev *dev, struct kpb_client *cli) } #include +#include void z_vrfy_kpb_notifier_init(struct comp_dev *dev, struct kpb_client *cli) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + K_OOPS(K_SYSCALL_MEMORY_WRITE(dev, sizeof(*dev))); K_OOPS(K_SYSCALL_MEMORY_WRITE(cli, sizeof(*cli))); z_impl_kpb_notifier_init(dev, cli); diff --git a/src/debug/debug_stream/debug_stream_slot.c b/src/debug/debug_stream/debug_stream_slot.c index 9154fed0ea75..f501339d6612 100644 --- a/src/debug/debug_stream/debug_stream_slot.c +++ b/src/debug/debug_stream/debug_stream_slot.c @@ -14,6 +14,7 @@ #ifdef CONFIG_USERSPACE #include +#include #include #endif @@ -136,6 +137,8 @@ int z_impl_debug_stream_slot_send_record(struct debug_stream_record *rec) #ifdef CONFIG_USERSPACE static inline int z_vrfy_debug_stream_slot_send_record(struct debug_stream_record *rec) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + uint32_t size_words; size_t bytes; diff --git a/src/ipc/ipc-common.c b/src/ipc/ipc-common.c index dd8443c50067..d24c20a130b5 100644 --- a/src/ipc/ipc-common.c +++ b/src/ipc/ipc-common.c @@ -331,6 +331,7 @@ EXPORT_SYMBOL(ipc_msg_send); #ifdef CONFIG_USERSPACE void z_vrfy_ipc_msg_send(struct ipc_msg *msg, void *data, bool high_priority) { + /* No caller check: DP modules send notifications, see mod_ipc_msg_init() */ K_OOPS(K_SYSCALL_MEMORY_WRITE(msg, sizeof(*msg))); K_OOPS(K_SYSCALL_VERIFY(msg->tx_size <= SOF_IPC_MSG_MAX_SIZE)); @@ -364,6 +365,7 @@ void z_impl_ipc_msg_list_remove(struct ipc_msg *msg) #ifdef CONFIG_USERSPACE void z_vrfy_ipc_msg_list_remove(struct ipc_msg *msg) { + /* No caller check: called by mod_ipc_msg_free() in DP module threads */ struct ipc *ipc = ipc_get(); struct list_item *mlist, *_mlist; bool found = false; diff --git a/src/ipc/ipc4/handler-kernel.c b/src/ipc/ipc4/handler-kernel.c index 649592c30d20..29b25bdf5d7d 100644 --- a/src/ipc/ipc4/handler-kernel.c +++ b/src/ipc/ipc4/handler-kernel.c @@ -43,6 +43,7 @@ #ifdef __ZEPHYR__ #include #include +#include #endif #include @@ -152,6 +153,8 @@ void z_impl_ipc_compound_pre_start(int msg_id) */ void z_vrfy_ipc_compound_pre_start(int msg_id) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_IPC); + z_impl_ipc_compound_pre_start(msg_id); } #include @@ -183,6 +186,8 @@ void z_impl_ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed) */ void z_vrfy_ipc_compound_post_start(uint32_t msg_id, int ret, bool delayed) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_IPC); + z_impl_ipc_compound_post_start(msg_id, ret, delayed); } #include @@ -243,6 +248,8 @@ int z_impl_ipc_wait_for_compound_msg(void) */ int z_vrfy_ipc_wait_for_compound_msg(void) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_IPC); + return z_impl_ipc_wait_for_compound_msg(); } #include @@ -588,6 +595,8 @@ void z_impl_ipc_msg_reply(struct sof_ipc_reply *reply) #ifdef CONFIG_USERSPACE void z_vrfy_ipc_msg_reply(struct sof_ipc_reply *reply) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + K_OOPS(K_SYSCALL_MEMORY_READ(reply, sizeof(*reply))); z_impl_ipc_msg_reply(reply); } diff --git a/src/ipc/ipc4/notification.c b/src/ipc/ipc4/notification.c index 86fb55e4d12f..196771ecc0c4 100644 --- a/src/ipc/ipc4/notification.c +++ b/src/ipc/ipc4/notification.c @@ -94,6 +94,12 @@ static inline bool z_vrfy_send_resource_notif(uint32_t resource_id, uint32_t eve uint32_t resource_type, void *data, uint32_t data_size) { + /* + * No caller check: DP threads report module processing errors with + * this, see dp_task_run(). The kernel picks the message, the caller + * only passes scalars and a bounded payload. + */ + /* Validate event_type is a known resource event */ K_OOPS(K_SYSCALL_VERIFY(event_type < SOF_IPC4_INVALID_RESOURCE_EVENT_TYPE)); diff --git a/src/library_manager/lib_manager.c b/src/library_manager/lib_manager.c index b42cbe0afc26..f2b25edd7ea2 100644 --- a/src/library_manager/lib_manager.c +++ b/src/library_manager/lib_manager.c @@ -790,11 +790,20 @@ __cold int lib_manager_mod_create_priv(const struct comp_driver *drv, #ifdef CONFIG_USERSPACE #include +#include static int z_vrfy_lib_manager_free_module(const uint32_t component_id) { #if CONFIG_SOF_USERSPACE_LL +#ifdef CONFIG_SOF_USERSPACE_SYSCALL_CALLER_CHECK + /* + * ipc_thread_user() reads a user-writable table, so it cannot + * identify the caller. Use the kernel-only registry instead. + */ + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_IPC); +#else K_OOPS(k_current_get() != ipc_thread_user(cpu_get_id())); +#endif return z_impl_lib_manager_free_module(component_id); #else ARG_UNUSED(component_id); diff --git a/src/schedule/zephyr_dp_schedule.c b/src/schedule/zephyr_dp_schedule.c index 314f8df5bba0..cb3148de20c7 100644 --- a/src/schedule/zephyr_dp_schedule.c +++ b/src/schedule/zephyr_dp_schedule.c @@ -249,8 +249,11 @@ void z_impl_scheduler_dp_ll_tick(void) #ifdef CONFIG_USERSPACE #include +#include void z_vrfy_scheduler_dp_ll_tick(void) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_LL); + z_impl_scheduler_dp_ll_tick(); } #include diff --git a/src/schedule/zephyr_dp_schedule_application.c b/src/schedule/zephyr_dp_schedule_application.c index 5e69d0943419..6c9dafc1db28 100644 --- a/src/schedule/zephyr_dp_schedule_application.c +++ b/src/schedule/zephyr_dp_schedule_application.c @@ -863,6 +863,7 @@ int scheduler_dp_task_init(struct task **task, const struct sof_uuid_entry *uid, #ifdef CONFIG_USERSPACE #include +#include /* * mod_free() follows the module's resource and allocator pointers in @@ -899,6 +900,8 @@ void z_vrfy_scheduler_dp_internal_free(struct task *task) { struct scheduler_dp_task_kobj kobj; + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + /* * Do not read task->priv_data: the kernel objects come from the * kernel-only record, and an unknown task pointer is rejected. diff --git a/src/schedule/zephyr_ll.c b/src/schedule/zephyr_ll.c index 3edebbca5b20..b83a4ae0f115 100644 --- a/src/schedule/zephyr_ll.c +++ b/src/schedule/zephyr_ll.c @@ -545,6 +545,7 @@ int z_impl_zephyr_ll_task_sem_free(struct task *task) #ifdef CONFIG_USERSPACE #include +#include /* * Read task->priv_data exactly once. The task lives in user memory and a * thread on another core can change the pointer after it has been checked. @@ -567,6 +568,8 @@ static inline int z_vrfy_zephyr_ll_task_sem_alloc(struct task *task) { struct zephyr_ll_pdata *pdata; + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + if (!task) return -EINVAL; pdata = zephyr_ll_task_pdata_vrfy(task); @@ -581,6 +584,8 @@ static inline int z_vrfy_zephyr_ll_task_sem_free(struct task *task) { struct zephyr_ll_pdata *pdata; + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + if (!task) return -EINVAL; pdata = zephyr_ll_task_pdata_vrfy(task); diff --git a/zephyr/syscall/alloc.c b/zephyr/syscall/alloc.c index 04f402f37d09..fec10fc39762 100644 --- a/zephyr/syscall/alloc.c +++ b/zephyr/syscall/alloc.c @@ -6,10 +6,13 @@ #include #include #include +#include static inline void *z_vrfy_sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, size_t alignment) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + /* only allow flags that are safe for user-space heap isolation */ static const uint32_t allowed_flags = SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT | SOF_MEM_FLAG_DMA | @@ -26,6 +29,8 @@ static inline void *z_vrfy_sof_heap_alloc(struct k_heap *heap, uint32_t flags, static inline void z_vrfy_sof_heap_free(struct k_heap *heap, void *addr) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + /* user-space use of sof_heap_alloc() limited to this single heap */ K_OOPS(!zephyr_ll_user_heap_verify(heap)); diff --git a/zephyr/syscall/dai.c b/zephyr/syscall/dai.c index 2e09187e146b..dcafcc389252 100644 --- a/zephyr/syscall/dai.c +++ b/zephyr/syscall/dai.c @@ -5,10 +5,13 @@ #include #include #include +#include static inline const struct device *z_vrfy_dai_get_device(enum sof_ipc_dai_type type, uint32_t index) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *dev = z_impl_dai_get_device(type, index); if (dev && !k_object_is_valid(dev, K_OBJ_DRIVER_DAI)) diff --git a/zephyr/syscall/sof_dma.c b/zephyr/syscall/sof_dma.c index 7e013728149b..25a27b093005 100644 --- a/zephyr/syscall/sof_dma.c +++ b/zephyr/syscall/sof_dma.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #ifdef CONFIG_SOF_USERSPACE_INTERFACE_DMA @@ -53,6 +54,8 @@ static const struct device *sof_dma_chan_vrfy(struct sof_dma *dma, uint32_t chan static inline struct sof_dma *z_vrfy_sof_dma_get(uint32_t dir, uint32_t cap, uint32_t dev, uint32_t flags) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + struct sof_dma *dma = z_impl_sof_dma_get(dir, cap, dev, flags); /* @@ -80,6 +83,8 @@ static inline struct sof_dma *z_vrfy_sof_dma_get(uint32_t dir, uint32_t cap, static inline void z_vrfy_sof_dma_put(struct sof_dma *dma) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + sof_dma_vrfy(dma); z_impl_sof_dma_put(dma); @@ -88,6 +93,8 @@ static inline void z_vrfy_sof_dma_put(struct sof_dma *dma) static inline int z_vrfy_sof_dma_get_attribute(struct sof_dma *dma, uint32_t type, uint32_t *value) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_vrfy(dma)->z_dev; K_OOPS(K_SYSCALL_MEMORY_WRITE(value, sizeof(*value))); @@ -98,6 +105,8 @@ static inline int z_vrfy_sof_dma_get_attribute(struct sof_dma *dma, uint32_t typ static inline int z_vrfy_sof_dma_request_channel(struct sof_dma *dma, uint32_t stream_tag) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_vrfy(dma)->z_dev; return dma_request_channel(z_dev, &stream_tag); @@ -107,6 +116,8 @@ static inline int z_vrfy_sof_dma_request_channel(struct sof_dma *dma, uint32_t s static inline void z_vrfy_sof_dma_release_channel(struct sof_dma *dma, uint32_t channel) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_chan_vrfy(dma, channel); dma_release_channel(z_dev, channel); @@ -254,6 +265,8 @@ static bool sof_dma_dir_allowed(const struct sof_dma_kinfo *kinfo, uint32_t dir) static inline int z_vrfy_sof_dma_config(struct sof_dma *dma, uint32_t channel, struct dma_config *config) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + struct dma_block_config *blk_cfgs; struct dma_config kern_cfg = { 0 }; struct dma_config user_cfg; @@ -296,6 +309,8 @@ static inline int z_vrfy_sof_dma_config(struct sof_dma *dma, uint32_t channel, static inline int z_vrfy_sof_dma_start(struct sof_dma *dma, uint32_t channel) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_chan_vrfy(dma, channel); return dma_start(z_dev, channel); @@ -304,6 +319,8 @@ static inline int z_vrfy_sof_dma_start(struct sof_dma *dma, uint32_t channel) static inline int z_vrfy_sof_dma_stop(struct sof_dma *dma, uint32_t channel) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_chan_vrfy(dma, channel); return dma_stop(z_dev, channel); @@ -313,6 +330,8 @@ static inline int z_vrfy_sof_dma_stop(struct sof_dma *dma, uint32_t channel) static inline int z_vrfy_sof_dma_get_status(struct sof_dma *dma, uint32_t channel, struct dma_status *stat) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_chan_vrfy(dma, channel); K_OOPS(K_SYSCALL_MEMORY_WRITE(stat, sizeof(*stat))); @@ -323,6 +342,8 @@ static inline int z_vrfy_sof_dma_get_status(struct sof_dma *dma, uint32_t channe static inline int z_vrfy_sof_dma_reload(struct sof_dma *dma, uint32_t channel, size_t size) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_chan_vrfy(dma, channel); return dma_reload(z_dev, channel, 0, 0, size); @@ -331,6 +352,8 @@ static inline int z_vrfy_sof_dma_reload(struct sof_dma *dma, uint32_t channel, s static inline int z_vrfy_sof_dma_suspend(struct sof_dma *dma, uint32_t channel) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_chan_vrfy(dma, channel); return dma_suspend(z_dev, channel); @@ -339,6 +362,8 @@ static inline int z_vrfy_sof_dma_suspend(struct sof_dma *dma, uint32_t channel) static inline int z_vrfy_sof_dma_resume(struct sof_dma *dma, uint32_t channel) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + const struct device *z_dev = sof_dma_chan_vrfy(dma, channel); return dma_resume(z_dev, channel); diff --git a/zephyr/syscall/vregion.c b/zephyr/syscall/vregion.c index 8593530149d8..e133c876eab0 100644 --- a/zephyr/syscall/vregion.c +++ b/zephyr/syscall/vregion.c @@ -5,6 +5,7 @@ #include #include #include +#include /* * Every validator pins the vregion with vregion_verify_pin() for the @@ -18,6 +19,8 @@ static inline void *z_vrfy_vregion_alloc_align(struct vregion *vr, size_t size, { void *ptr = NULL; + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + if (vregion_verify_pin(vr)) { ptr = z_impl_vregion_alloc_align(vr, size, alignment); vregion_unpin(vr); @@ -32,6 +35,8 @@ static inline void *z_vrfy_vregion_alloc_coherent_align(struct vregion *vr, { void *ptr = NULL; + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + if (vregion_verify_pin(vr)) { ptr = z_impl_vregion_alloc_coherent_align(vr, size, alignment); vregion_unpin(vr); @@ -43,6 +48,8 @@ static inline void *z_vrfy_vregion_alloc_coherent_align(struct vregion *vr, static inline void z_vrfy_vregion_free(struct vregion *vr, void *ptr) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + if (vregion_verify_pin(vr)) { z_impl_vregion_free(vr, ptr); vregion_unpin(vr); @@ -54,6 +61,8 @@ struct vregion *z_vrfy_vregion_get(struct vregion *vr) { bool ok; + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + if (!vregion_verify_pin(vr)) return NULL; @@ -68,6 +77,8 @@ struct vregion *z_vrfy_vregion_put(struct vregion *vr) { int ret; + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + if (!vregion_verify_pin(vr)) return NULL; @@ -84,6 +95,8 @@ struct vregion *z_vrfy_vregion_put(struct vregion *vr) struct vregion *z_vrfy_vregion_create_map(uintptr_t *vreg_start, size_t *vreg_size) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + K_OOPS(K_SYSCALL_MEMORY_WRITE(vreg_start, sizeof(*vreg_start))); K_OOPS(K_SYSCALL_MEMORY_WRITE(vreg_size, sizeof(*vreg_size))); return z_impl_vregion_create_map(vreg_start, vreg_size); @@ -92,6 +105,8 @@ struct vregion *z_vrfy_vregion_create_map(uintptr_t *vreg_start, size_t *vreg_si void z_vrfy_vregion_set_interim(struct vregion *vr) { + SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_INFRA); + if (vregion_verify_pin(vr)) { z_impl_vregion_set_interim(vr); vregion_unpin(vr); diff --git a/zephyr/test/userspace/test_intel_hda_dma.c b/zephyr/test/userspace/test_intel_hda_dma.c index 064cb2b115c0..ece586d650b7 100644 --- a/zephyr/test/userspace/test_intel_hda_dma.c +++ b/zephyr/test/userspace/test_intel_hda_dma.c @@ -18,6 +18,7 @@ */ #include +#include #include #include @@ -236,6 +237,11 @@ static void intel_hda_dma_kernel(void) hda_ipc_msg(IPCCMD_HDA_CONFIG, TEST_CHANNEL | (DMA_BUF_SIZE << 8), IPC_TIMEOUT); + /* + * DMA syscalls are restricted to the SOF infrastructure threads. IPC + * is disabled in standalone boot tests, so borrow the IPC role. + */ + sof_sysuser_thread_set(SOF_SYSUSER_IPC, PLATFORM_PRIMARY_CORE_ID, &user_thread); k_thread_start(&user_thread); LOG_INF("user started, waiting for it to be ready"); @@ -266,6 +272,7 @@ static void intel_hda_dma_kernel(void) LOG_INF("test done, terminate user thread"); k_thread_join(&user_thread, K_FOREVER); + sof_sysuser_thread_set(SOF_SYSUSER_IPC, PLATFORM_PRIMARY_CORE_ID, NULL); ret = ipc_service_deregister_endpoint(&host_ipc_ept); zassert_equal(ret, 0, "cannot deregister IPC endpoint: %d", ret); diff --git a/zephyr/test/userspace/test_intel_ssp_dai.c b/zephyr/test/userspace/test_intel_ssp_dai.c index 6c700c3839bb..177e2c185b15 100644 --- a/zephyr/test/userspace/test_intel_ssp_dai.c +++ b/zephyr/test/userspace/test_intel_ssp_dai.c @@ -19,6 +19,7 @@ */ #include +#include #include #include @@ -304,6 +305,11 @@ static void intel_ssp_dai_kernel(void) k_thread_access_grant(&user_thread, dma_in); k_thread_access_grant(&user_thread, dai_dev); + /* + * DMA syscalls are restricted to the SOF infrastructure threads. IPC + * is disabled in standalone boot tests, so borrow the IPC role. + */ + sof_sysuser_thread_set(SOF_SYSUSER_IPC, PLATFORM_PRIMARY_CORE_ID, &user_thread); k_thread_start(&user_thread); LOG_INF("user started, waiting for it to be ready"); @@ -324,6 +330,7 @@ static void intel_ssp_dai_kernel(void) LOG_INF("test done, terminate user thread"); k_thread_join(&user_thread, K_FOREVER); + sof_sysuser_thread_set(SOF_SYSUSER_IPC, PLATFORM_PRIMARY_CORE_ID, NULL); } ZTEST(userspace_intel_dai_ssp, dai_ssp_loopback_setup) From d72dbd51fdf00939422959f1b31b25649b6f7ecf Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 21 Oct 2026 16:17:09 +0300 Subject: [PATCH 3/4] userspace: doc: document syscall caller restrictions Add the missing kpb_notifier_init and mod_data_blob_set entries to the syscall map, document which threads may call which syscalls, and extend the rules for adding syscalls with the caller check, single fetch of user data and not following user-writable pointers in the kernel. Signed-off-by: Kai Vehmanen --- src/include/sof/userspace/README.md | 31 +++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/include/sof/userspace/README.md b/src/include/sof/userspace/README.md index ac26aa07ab6d..d0f69eb83d54 100644 --- a/src/include/sof/userspace/README.md +++ b/src/include/sof/userspace/README.md @@ -122,10 +122,33 @@ syscall by subsystem: | DMA | `sof_dma_get`, `sof_dma_put`, `sof_dma_get_attribute`, `sof_dma_request_channel`, `sof_dma_release_channel`, `sof_dma_config`, `sof_dma_start`, `sof_dma_stop`, `sof_dma_get_status`, `sof_dma_reload`, `sof_dma_suspend`, `sof_dma_resume` | `sof/lib/sof_dma.h` | `zephyr/syscall/sof_dma.c` / `lib/dma.c` (get/put); remaining 10 are `static inline z_impl_*` in `sof/lib/sof_dma.h` | | DAI | `dai_get_device` | `sof/lib/dai-zephyr.h` | `zephyr/syscall/dai.c` / `lib/dai.c` | | CPU | `cpu_get_id` | `sof/lib/cpu.h` | `zephyr/syscall/cpu.c` / `zephyr/lib/cpu.c` | -| Module management | `mod_balloc_align`, `mod_alloc_ext`, `mod_free`, `mod_free_all`, `mod_data_blob_handler_new`, `mod_fast_get` | `sof/audio/module_adapter/module/generic.h` | scattered: `audio/module_adapter/module/generic.c` | +| Module management | `mod_balloc_align`, `mod_alloc_ext`, `mod_free`, `mod_free_all`, `mod_data_blob_handler_new`, `mod_data_blob_set`, `mod_fast_get` | `sof/audio/module_adapter/module/generic.h` | scattered: `audio/module_adapter/module/generic.c` | | Library manager | `lib_manager_free_module` | `sof/lib_manager.h` | scattered: `library_manager/lib_manager.c` | | Scheduling | `zephyr_ll_task_sem_alloc`, `zephyr_ll_task_sem_free`, `scheduler_dp_internal_free`, `scheduler_dp_ll_tick` | `sof/schedule/ll_schedule_domain.h`, `sof/schedule/dp_schedule.h` | scattered: `schedule/zephyr_ll.c`, `schedule/zephyr_dp_schedule*.c` | | Debug | `debug_stream_slot_send_record` | `user/debug_stream_slot.h` | scattered: `debug/debug_stream/debug_stream_slot.c` | +| KPB | `kpb_notifier_init` | `sof/audio/kpb.h` | scattered: `audio/kpb.c` | + +### Allowed callers + +Zephyr lets any user thread invoke any syscall. With +`CONFIG_SOF_USERSPACE_SYSCALL_CALLER_CHECK` (default `y` with `SOF_USERSPACE_LL`), each +validator first calls `SOF_SYSCALL_CALLER_CHECK()` from +[`sof/userspace/syscall_caller.h`](syscall_caller.h). That kills any calling thread that is +not registered with an allowed role. The registry is kernel-only; never identify the caller +from user-writable data such as `ipc_user->thread[]`. + +| Allowed callers | Syscalls | +|-----------------|----------| +| Any user thread, including DP module threads | `cpu_get_id`, all module management syscalls (`mod_*`), `ipc_msg_send`, `ipc_msg_list_remove`, `send_resource_notif` | +| IPC user thread only | `ipc_compound_pre_start`, `ipc_compound_post_start`, `ipc_wait_for_compound_msg`, `lib_manager_free_module` | +| LL user thread only | `scheduler_dp_ll_tick` | +| LL or IPC user thread | all other SOF syscalls | + +DP modules only get the syscalls needed to implement the module interface. That includes +sending notifications: modules queue messages from `mod_ipc_msg_init()` with +`ipc_msg_send()` and free them with `mod_ipc_msg_free()`, and `dp_task_run()` reports +processing errors with `send_resource_notif()`. DP modules have no access to the other IPC +syscalls or to heap, vregion, DMA, DAI or scheduler syscalls. The `zephyr/syscall/*.c` files hold the validators in one place per subsystem; the rows marked **scattered** keep their `z_vrfy_` next to the kernel logic and are the migration targets. @@ -200,7 +223,11 @@ Key options: `audio/`. Reusable pure code → a shared-library directory (keep it user-safe). 2. **Cross the boundary only via a syscall.** Add a `__syscall` declaration, put the `z_vrfy_` validator in the boundary tier — preferably in `zephyr/syscall/.c` - alongside the existing ones — and validate every argument. + alongside the existing ones — and validate every argument. Start the validator with + `SOF_SYSCALL_CALLER_CHECK()` and add the syscall to the + [allowed callers](#allowed-callers) table. Read each field of a user structure once + (`k_usermode_from_copy()`), and never let the kernel follow, free through or link a + pointer that lives in user-writable memory. 3. **Mark shared globals.** Any global a kernel-managed user thread (LL user thread, userspace IPC thread, proxy worker) reads/writes gets `APP_TASK_*`, `APP_SYSUSER_*`, or a `K_APP_*` partition marker — and the kernel re-validates it. DP modules in application mode use their From c171548c7e88b520f49907ef3bdc63d5cec1784e Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 21 Oct 2026 16:17:09 +0300 Subject: [PATCH 4/4] zephyr: test: add negative tests for syscall validators Add standalone boot tests that check the caller restriction, rejection of a forged vregion handle, and the DMA channel range and transfer direction checks. Cases that need features the target lacks are skipped. Signed-off-by: Kai Vehmanen --- zephyr/test/CMakeLists.txt | 5 + zephyr/test/userspace/test_syscall_caller.c | 241 ++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 zephyr/test/userspace/test_syscall_caller.c diff --git a/zephyr/test/CMakeLists.txt b/zephyr/test/CMakeLists.txt index 547502d84ae4..e64b61314da4 100644 --- a/zephyr/test/CMakeLists.txt +++ b/zephyr/test/CMakeLists.txt @@ -32,3 +32,8 @@ endif() if(CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_LL) zephyr_library_sources(userspace/test_ll_task.c) endif() + +if(CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_SYSCALL_CALLER_CHECK AND + CONFIG_IPC_MAJOR_4) + zephyr_library_sources(userspace/test_syscall_caller.c) +endif() diff --git a/zephyr/test/userspace/test_syscall_caller.c b/zephyr/test/userspace/test_syscall_caller.c new file mode 100644 index 000000000000..2e70d2fe5624 --- /dev/null +++ b/zephyr/test/userspace/test_syscall_caller.c @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright(c) 2026 Intel Corporation. + */ + +/* + * Negative tests for SOF system call validators: the caller restriction + * (CONFIG_SOF_USERSPACE_SYSCALL_CALLER_CHECK), forged vregion handles and + * DMA channel and direction checks. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG); + +#define USER_STACKSIZE 2048 + +static struct k_thread user_thread; +static K_THREAD_STACK_DEFINE(user_stack, USER_STACKSIZE); + +/* kernel-only object whose address is passed as a forged handle */ +static uint32_t kernel_only_word[16]; + +enum caller_test_case { + /* allowed for any user thread */ + CALLER_ANY_CPU_GET_ID, + /* restricted syscall from an unregistered thread */ + CALLER_UNREGISTERED_IPC_SYSCALL, + /* registered as IPC thread, calling an IPC-only syscall */ + CALLER_IPC_IPC_SYSCALL, + /* registered as IPC thread, calling an LL-only syscall */ + CALLER_IPC_LL_SYSCALL, + /* registered, passing a kernel address as a vregion handle */ + CALLER_IPC_FORGED_VREGION, + /* registered, starting a DMA channel number out of range */ + CALLER_IPC_DMA_BAD_CHANNEL, + /* registered, DMA config direction not supported by the device */ + CALLER_IPC_DMA_BAD_DIRECTION, +}; + +#define HAVE_HDA_HOST_DMA \ + (CONFIG_SOF_USERSPACE_INTERFACE_DMA && CONFIG_DT_HAS_INTEL_ADSP_HDA_HOST_IN_ENABLED) + +#if HAVE_HDA_HOST_DMA +/* + * The DMA handle and channel are taken and released by the kernel side of + * the test, so a user thread killed by the validator leaks nothing and the + * suite can be run again. + */ +static struct sof_dma *test_dma; +static int test_channel; + +static void dma_config_wrong_direction(struct sof_dma *dma, int channel) +{ + uint8_t buf[64] __aligned(64); + struct dma_block_config blk = { + .source_address = (uintptr_t)buf, + .dest_address = (uintptr_t)buf, + .block_size = sizeof(buf), + }; + struct dma_config cfg = { + /* the device only supports LMEM_TO_HMEM (MEMORY_TO_HOST) */ + .channel_direction = HOST_TO_MEMORY, + .block_count = 1, + .head_block = &blk, + }; + + (void)sof_dma_config(dma, channel, &cfg); +} +#endif + +static void user_function(void *p1, void *p2, void *p3) +{ + enum caller_test_case test_case = (enum caller_test_case)(uintptr_t)p1; +#if HAVE_HDA_HOST_DMA + struct sof_dma *dma = p2; + int channel = (int)(uintptr_t)p3; +#endif + + zassert_true(k_is_user_context(), "isn't user"); + + switch (test_case) { + case CALLER_ANY_CPU_GET_ID: + zassert_true(cpu_get_id() < CONFIG_CORE_COUNT); + return; + case CALLER_UNREGISTERED_IPC_SYSCALL: + (void)ipc_wait_for_compound_msg(); + break; + case CALLER_IPC_IPC_SYSCALL: + zassert_equal(ipc_wait_for_compound_msg(), 0); + return; + case CALLER_IPC_LL_SYSCALL: +#if CONFIG_ZEPHYR_DP_SCHEDULER + scheduler_dp_ll_tick(); + break; +#else + ztest_test_skip(); + return; +#endif + case CALLER_IPC_FORGED_VREGION: +#if CONFIG_SOF_USERSPACE_INTERFACE_VREGION + (void)vregion_get((struct vregion *)kernel_only_word); + break; +#else + ztest_test_skip(); + return; +#endif + case CALLER_IPC_DMA_BAD_CHANNEL: +#if HAVE_HDA_HOST_DMA + (void)sof_dma_start(dma, 0x10000); + break; +#else + ztest_test_skip(); + return; +#endif + case CALLER_IPC_DMA_BAD_DIRECTION: +#if HAVE_HDA_HOST_DMA + dma_config_wrong_direction(dma, channel); + break; +#else + ztest_test_skip(); + return; +#endif + default: + zassert_unreachable("unknown test case"); + } + + zassert_unreachable("syscall security check did not fault"); +} + +static void run_case(enum caller_test_case test_case, bool register_ipc, bool expect_fault) +{ + void *dma = NULL; + uintptr_t channel = 0; + int ret; + +#if HAVE_HDA_HOST_DMA + if (test_case == CALLER_IPC_DMA_BAD_CHANNEL || test_case == CALLER_IPC_DMA_BAD_DIRECTION) { + test_dma = sof_dma_get(SOF_DMA_DIR_LMEM_TO_HMEM, 0, SOF_DMA_DEV_HOST, + SOF_DMA_ACCESS_SHARED); + zassert_not_null(test_dma, "sof_dma_get failed"); + test_channel = sof_dma_request_channel(test_dma, 0); + zassert_true(test_channel >= 0, "sof_dma_request_channel failed: %d", + test_channel); + dma = test_dma; + channel = test_channel; + } +#endif + + k_thread_create(&user_thread, user_stack, USER_STACKSIZE, + user_function, (void *)(uintptr_t)test_case, dma, (void *)channel, + -1, K_USER, K_FOREVER); + k_mem_domain_add_thread(zephyr_ll_mem_domain(), &user_thread); +#if HAVE_HDA_HOST_DMA + user_grant_dma_access_all(&user_thread); +#endif + + /* IPC is disabled in standalone boot tests, so the IPC role is free */ + if (register_ipc) + sof_sysuser_thread_set(SOF_SYSUSER_IPC, PLATFORM_PRIMARY_CORE_ID, &user_thread); + if (expect_fault) + sof_boot_test_set_fault_valid(&user_thread); + + k_thread_start(&user_thread); + ret = k_thread_join(&user_thread, K_FOREVER); + + sof_sysuser_thread_set(SOF_SYSUSER_IPC, PLATFORM_PRIMARY_CORE_ID, NULL); + sof_boot_test_set_fault_valid(NULL); + +#if HAVE_HDA_HOST_DMA + if (dma) { + sof_dma_release_channel(test_dma, test_channel); + sof_dma_put(test_dma); + test_dma = NULL; + } +#endif + + zassert_equal(ret, 0, "user thread join failed: %d", ret); +} + +ZTEST(userspace_syscall_caller, cpu_get_id_any_thread) +{ + run_case(CALLER_ANY_CPU_GET_ID, false, false); +} + +ZTEST(userspace_syscall_caller, unregistered_thread_rejected) +{ + run_case(CALLER_UNREGISTERED_IPC_SYSCALL, false, true); +} + +ZTEST(userspace_syscall_caller, ipc_thread_allowed) +{ + run_case(CALLER_IPC_IPC_SYSCALL, true, false); +} + +ZTEST(userspace_syscall_caller, ipc_thread_ll_syscall_rejected) +{ + run_case(CALLER_IPC_LL_SYSCALL, true, true); +} + +ZTEST(userspace_syscall_caller, forged_vregion_rejected) +{ + run_case(CALLER_IPC_FORGED_VREGION, true, true); +} + +ZTEST(userspace_syscall_caller, dma_bad_channel_rejected) +{ + run_case(CALLER_IPC_DMA_BAD_CHANNEL, true, true); +} + +ZTEST(userspace_syscall_caller, dma_bad_direction_rejected) +{ + run_case(CALLER_IPC_DMA_BAD_DIRECTION, true, true); +} + +ZTEST_SUITE(userspace_syscall_caller, NULL, NULL, NULL, NULL, NULL); + +/** + * SOF main has booted up and IPC handling is stopped. + * Run test suites with ztest_run_all. + */ +static int run_tests(void) +{ + ztest_run_test_suite(userspace_syscall_caller, false, 1, 1, NULL); + return 0; +} + +SYS_INIT(run_tests, APPLICATION, 99);