Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/audio/kpb.c
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,11 @@ void z_impl_kpb_notifier_init(struct comp_dev *dev, struct kpb_client *cli)
}

#include <zephyr/internal/syscall_handler.h>
#include <sof/userspace/syscall_caller.h>
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);
Expand Down
3 changes: 3 additions & 0 deletions src/debug/debug_stream/debug_stream_slot.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#ifdef CONFIG_USERSPACE
#include <zephyr/internal/syscall_handler.h>
#include <sof/userspace/syscall_caller.h>
#include <zephyr/sys/math_extras.h>
#endif

Expand Down Expand Up @@ -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;

Expand Down
31 changes: 29 additions & 2 deletions src/include/sof/userspace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/<subsystem>.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
Expand Down
73 changes: 73 additions & 0 deletions src/include/sof/userspace/syscall_caller.h
Original file line number Diff line number Diff line change
@@ -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 <stdbool.h>

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__ */
4 changes: 4 additions & 0 deletions src/ipc/ipc-common.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#endif

#include <sof/debug/telemetry/performance_monitor.h>
#include <sof/userspace/syscall_caller.h>

LOG_MODULE_REGISTER(ipc, CONFIG_SOF_LOG_LEVEL);

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/ipc/ipc4/handler-kernel.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#ifdef __ZEPHYR__
#include <zephyr/kernel.h>
#include <zephyr/internal/syscall_handler.h>
#include <sof/userspace/syscall_caller.h>
#endif

#include <errno.h>
Expand Down Expand Up @@ -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);
Comment thread
kv2019i marked this conversation as resolved.
Resolved

z_impl_ipc_compound_pre_start(msg_id);
}
#include <zephyr/syscalls/ipc_compound_pre_start_mrsh.c>
Expand Down Expand Up @@ -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 <zephyr/syscalls/ipc_compound_post_start_mrsh.c>
Expand Down Expand Up @@ -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 <zephyr/syscalls/ipc_wait_for_compound_msg_mrsh.c>
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions src/ipc/ipc4/notification.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
9 changes: 9 additions & 0 deletions src/library_manager/lib_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -790,11 +790,20 @@ __cold int lib_manager_mod_create_priv(const struct comp_driver *drv,

#ifdef CONFIG_USERSPACE
#include <zephyr/internal/syscall_handler.h>
#include <sof/userspace/syscall_caller.h>

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);
Expand Down
26 changes: 17 additions & 9 deletions src/schedule/zephyr_domain.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <sof/schedule/ll_schedule.h>
#include <sof/schedule/ll_schedule_domain.h>
#include <sof/schedule/schedule.h>
#include <sof/userspace/syscall_caller.h>
#include <rtos/task.h>
#include <ipc/topology.h>
#include <limits.h>
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions src/schedule/zephyr_dp_schedule.c
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,11 @@ void z_impl_scheduler_dp_ll_tick(void)

#ifdef CONFIG_USERSPACE
#include <zephyr/internal/syscall_handler.h>
#include <sof/userspace/syscall_caller.h>
void z_vrfy_scheduler_dp_ll_tick(void)
{
SOF_SYSCALL_CALLER_CHECK(SOF_SYSUSER_LL);

z_impl_scheduler_dp_ll_tick();
}
#include <zephyr/syscalls/scheduler_dp_ll_tick_mrsh.c>
Expand Down
3 changes: 3 additions & 0 deletions src/schedule/zephyr_dp_schedule_application.c
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,7 @@ int scheduler_dp_task_init(struct task **task, const struct sof_uuid_entry *uid,

#ifdef CONFIG_USERSPACE
#include <zephyr/internal/syscall_handler.h>
#include <sof/userspace/syscall_caller.h>

/*
* mod_free() follows the module's resource and allocator pointers in
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/schedule/zephyr_ll.c
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ int z_impl_zephyr_ll_task_sem_free(struct task *task)

#ifdef CONFIG_USERSPACE
#include <zephyr/internal/syscall_handler.h>
#include <sof/userspace/syscall_caller.h>
/*
* 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.
Expand All @@ -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);
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions zephyr/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading