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/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 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..d24c20a130b5 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); @@ -330,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)); @@ -363,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; @@ -592,6 +595,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/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_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/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/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/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/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; +} 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/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_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) 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);