From 9125b4671679a52073d955ccf666557cd28d7a70 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 11 Nov 2026 13:19:36 +0200 Subject: [PATCH 1/5] zephyr: ll_user: reserve kernel-managed areas in the LL memory region The LL user heap is a k_heap whose sys_heap metadata is in memory that the LL and IPC user threads can write, while kernel code allocates and frees from it. A user thread can therefore forge the metadata that the kernel allocator follows. As a first step towards keeping the kernel out of that heap, reserve two page-aligned areas at the start of the LL memory region: one mapped read-write and one mapped read-only into the LL memory domain. Kernel code will allocate the objects that LL user threads must access from these areas, with allocator state kept in kernel memory. The sizes are set with CONFIG_SOF_ZEPHYR_LL_KAREA_RW_SIZE and CONFIG_SOF_ZEPHYR_LL_KAREA_RO_SIZE. They are taken from the region size CONFIG_SOF_ZEPHYR_SYS_USER_HEAP_SIZE, so the kernel heap, from which the region is allocated, is not affected. The LL user heap gets the rest. Signed-off-by: Kai Vehmanen --- src/include/sof/schedule/ll_schedule_domain.h | 7 + src/kernel/schedule/zephyr_ll_user.c | 122 ++++++++++++------ zephyr/Kconfig | 33 ++++- 3 files changed, 117 insertions(+), 45 deletions(-) diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index c369242640af..05e91427cda5 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -227,6 +227,13 @@ bool zephyr_ll_user_heap_verify(struct k_heap *heap); */ void zephyr_ll_user_resources_init(void); +/** @brief Kernel-managed areas of the LL user memory region */ +enum zephyr_ll_karea { + ZEPHYR_LL_KAREA_RW, /**< read-write for LL user threads */ + ZEPHYR_LL_KAREA_RO, /**< read-only for LL user threads */ + ZEPHYR_LL_KAREA_COUNT, +}; + /** * @brief Grant thread access to per-core Low-Latency mutexes. * @param[in,out] thread Target thread. diff --git a/src/kernel/schedule/zephyr_ll_user.c b/src/kernel/schedule/zephyr_ll_user.c index 1aa56c44e461..9422023c355b 100644 --- a/src/kernel/schedule/zephyr_ll_user.c +++ b/src/kernel/schedule/zephyr_ll_user.c @@ -12,6 +12,35 @@ LOG_MODULE_DECLARE(ll_schedule, CONFIG_SOF_LOG_LEVEL); +/* + * Layout of the LL user memory region, each part page aligned: + * + * | kernel RO area | kernel RW area | LL user heap | + * + * The heap and the kernel RW area are mapped read-write into the LL memory + * domain, the kernel RO area read-only. Kernel code allocates objects that + * LL user threads must access from the kernel areas, see + * zephyr_ll_karea_alloc(). The allocator state of those areas is kept in + * ll_mem_resources, which is kernel-only. + */ +#define LL_REGION_SIZE CONFIG_SOF_ZEPHYR_SYS_USER_HEAP_SIZE +#define LL_KAREA_RO_SIZE CONFIG_SOF_ZEPHYR_LL_KAREA_RO_SIZE +#define LL_KAREA_RW_SIZE CONFIG_SOF_ZEPHYR_LL_KAREA_RW_SIZE +#define LL_HEAP_SIZE (LL_REGION_SIZE - LL_KAREA_RO_SIZE - LL_KAREA_RW_SIZE) + +BUILD_ASSERT(LL_REGION_SIZE % CONFIG_MM_DRV_PAGE_SIZE == 0); +BUILD_ASSERT(LL_KAREA_RO_SIZE % CONFIG_MM_DRV_PAGE_SIZE == 0); +BUILD_ASSERT(LL_KAREA_RW_SIZE % CONFIG_MM_DRV_PAGE_SIZE == 0); +BUILD_ASSERT(LL_KAREA_RO_SIZE + LL_KAREA_RW_SIZE < LL_REGION_SIZE, + "kernel areas do not leave room for the LL user heap"); + +/* A kernel-managed area, addresses are the uncached alias */ +struct zephyr_ll_karea_state { + uint8_t *base; + size_t size; + size_t used; +}; + /** * Memory resources for userspace LL scheduler * @@ -22,64 +51,73 @@ LOG_MODULE_DECLARE(ll_schedule, CONFIG_SOF_LOG_LEVEL); struct zephyr_ll_mem_resources { struct k_mem_domain mem_domain; /**< Memory domain for LL thread isolation */ struct k_heap heap; /**< Heap allocator for LL scheduler memory */ + struct zephyr_ll_karea_state karea[ZEPHYR_LL_KAREA_COUNT]; /**< Kernel-managed areas */ }; static struct zephyr_ll_mem_resources ll_mem_resources; -static struct k_heap *ll_heap_alloc(void) +static void ll_region_map(uint8_t *start, size_t size, k_mem_partition_attr_t attr) { - const size_t alloc_size = CONFIG_SOF_ZEPHYR_SYS_USER_HEAP_SIZE; - - BUILD_ASSERT(CONFIG_SOF_ZEPHYR_SYS_USER_HEAP_SIZE % CONFIG_MM_DRV_PAGE_SIZE == 0); + struct k_mem_partition part; + int ret; - void *mem = rballoc_align(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, alloc_size, - CONFIG_MM_DRV_PAGE_SIZE); - if (!mem) - return NULL; + part.start = (uintptr_t)sys_cache_cached_ptr_get(start); + part.size = size; + part.attr = attr | XTENSA_MMU_CACHED_WB; - k_heap_init(&ll_mem_resources.heap, mem, alloc_size); + ret = k_mem_domain_add_partition(&ll_mem_resources.mem_domain, &part); + tr_dbg(&ll_tr, "map %p size %#zx (cached), ret %d", (void *)part.start, size, ret); + if (ret) + k_panic(); - /* - * k_heap_init() does not set these, so set the values - * manually here - */ - ll_mem_resources.heap.heap.init_mem = mem; - ll_mem_resources.heap.heap.init_bytes = alloc_size; +#ifdef CONFIG_CACHE_HAS_MIRRORED_MEMORY_REGIONS + part.start = (uintptr_t)sys_cache_uncached_ptr_get(start); + part.attr = attr; - return &ll_mem_resources.heap; + ret = k_mem_domain_add_partition(&ll_mem_resources.mem_domain, &part); + tr_dbg(&ll_tr, "map %p size %#zx (uncached), ret %d", (void *)part.start, size, ret); + if (ret) + k_panic(); +#endif } -static void ll_heap_init(void) +static void ll_region_init(void) { - struct k_heap *heap = ll_heap_alloc(); - struct k_mem_partition mem_partition; - int ret; - - if (!heap) { - tr_err(&ll_tr, "heap alloc fail"); + struct zephyr_ll_karea_state *ro = &ll_mem_resources.karea[ZEPHYR_LL_KAREA_RO]; + struct zephyr_ll_karea_state *rw = &ll_mem_resources.karea[ZEPHYR_LL_KAREA_RW]; + uint8_t *mem; + + mem = rballoc_align(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT, LL_REGION_SIZE, + CONFIG_MM_DRV_PAGE_SIZE); + if (!mem) { + tr_err(&ll_tr, "region alloc fail"); k_panic(); } - /* Create memory partition for sch_data array */ - mem_partition.start = (uintptr_t)sys_cache_cached_ptr_get(heap->heap.init_mem); - mem_partition.size = heap->heap.init_bytes; - mem_partition.attr = K_MEM_PARTITION_P_RW_U_RW | XTENSA_MMU_CACHED_WB; + ro->base = mem; + ro->size = LL_KAREA_RO_SIZE; + rw->base = mem + LL_KAREA_RO_SIZE; + rw->size = LL_KAREA_RW_SIZE; - ret = k_mem_domain_add_partition(&ll_mem_resources.mem_domain, &mem_partition); - tr_dbg(&ll_tr, "init ll heap %p, size %u (cached), ret %d", - (void *)mem_partition.start, heap->heap.init_bytes, ret); - if (ret) - k_panic(); + mem += LL_KAREA_RO_SIZE + LL_KAREA_RW_SIZE; + k_heap_init(&ll_mem_resources.heap, mem, LL_HEAP_SIZE); -#ifdef CONFIG_CACHE_HAS_MIRRORED_MEMORY_REGIONS - mem_partition.start = (uintptr_t)sys_cache_uncached_ptr_get(heap->heap.init_mem); - mem_partition.attr = K_MEM_PARTITION_P_RW_U_RW; - ret = k_mem_domain_add_partition(&ll_mem_resources.mem_domain, &mem_partition); - tr_dbg(&ll_tr, "init ll heap %p, size %u (uncached), ret %d", - (void *)mem_partition.start, heap->heap.init_bytes, ret); - if (ret) - k_panic(); -#endif + /* + * k_heap_init() does not set these, so set the values + * manually here + */ + ll_mem_resources.heap.heap.init_mem = mem; + ll_mem_resources.heap.heap.init_bytes = LL_HEAP_SIZE; + + if (ro->size) + ll_region_map(ro->base, ro->size, K_MEM_PARTITION_P_RO_U_RO); + + /* the kernel RW area and the heap are adjacent, map them together */ + ll_region_map(rw->base, rw->size + LL_HEAP_SIZE, K_MEM_PARTITION_P_RW_U_RW); + + tr_info(&ll_tr, "LL region: RO area %p %#zx, RW area %p %#zx, heap %p %#zx", + (void *)ro->base, ro->size, (void *)rw->base, rw->size, (void *)mem, + (size_t)LL_HEAP_SIZE); } void zephyr_ll_user_resources_init(void) @@ -88,7 +126,7 @@ void zephyr_ll_user_resources_init(void) k_mem_domain_init(&ll_mem_resources.mem_domain, 0, NULL); - ll_heap_init(); + ll_region_init(); /* attach common partition to LL domain */ user_memory_attach_common_partition(zephyr_ll_mem_domain()); diff --git a/zephyr/Kconfig b/zephyr/Kconfig index fdefb6182e3f..8408996c68bd 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -166,16 +166,43 @@ config SOF_ZEPHYR_USERSPACE_MODULE_HEAP_SIZE shared between instances of the same module. config SOF_ZEPHYR_SYS_USER_HEAP_SIZE - hex "Size of the shared LL user-space heap" + hex "Size of the LL user-space memory region" default 0x20000 depends on SOF_USERSPACE_LL help - The size of the shared heap used by the low-latency (LL) scheduler - user-space thread. This heap is shared across all pipelines running + The size of the memory region of the low-latency (LL) scheduler + user-space threads: the shared LL user heap and the kernel-managed + areas SOF_ZEPHYR_LL_KAREA_RW_SIZE and SOF_ZEPHYR_LL_KAREA_RO_SIZE. + The heap is shared across all pipelines running on the LL thread and must be large enough to hold all host DMA buffers, chain DMA data, SG elements, and module adapter buffers for all simultaneously active pipelines. +config SOF_ZEPHYR_LL_KAREA_RW_SIZE + hex "Size of the kernel-managed read-write area for LL user threads" + default 0x4000 + depends on SOF_USERSPACE_LL + help + Size of the area in the LL user memory region from which kernel + code allocates objects that the LL and IPC user threads read and + write. The kernel keeps the allocator state for this area in its + own memory, so user threads cannot affect kernel allocations. + The area is taken from SOF_ZEPHYR_SYS_USER_HEAP_SIZE. Must be a + multiple of the MMU page size. + +config SOF_ZEPHYR_LL_KAREA_RO_SIZE + hex "Size of the kernel-managed read-only area for LL user threads" + default 0x1000 + depends on SOF_USERSPACE_LL + help + Size of the area in the LL user memory region from which kernel + code allocates objects that the LL and IPC user threads may only + read. The area is mapped read-only into the LL memory domain, so + kernel code running in that domain, for example in a system call, + cannot write it either. The area is taken from + SOF_ZEPHYR_SYS_USER_HEAP_SIZE. Must be a multiple of the MMU page + size. + config SOF_USERSPACE_PROXY bool "Use userspace proxy to support userspace modules" select SOF_USERSPACE_USE_DRIVER_HEAP From 4a48cf0eb0f90c2f89f24048a0328206e352d4b7 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 11 Nov 2026 13:21:57 +0200 Subject: [PATCH 2/5] zephyr: ll_user: add an allocator for the kernel-managed LL areas Add zephyr_ll_karea_alloc() for kernel code that creates objects in the LL memory region. It is a bump allocator whose state is in kernel-only memory, so nothing that user threads write can affect it. Allocations are cache line aligned and sized, and return the cached alias unless SOF_MEM_FLAG_COHERENT is given, like sof_heap_alloc(). Memory is never freed, which suits the objects created at boot and at secondary core bring-up that live until the firmware is reset. Add zephyr_ll_karea_contains() for checking that memory lies in an area. Signed-off-by: Kai Vehmanen --- src/include/sof/schedule/ll_schedule_domain.h | 28 ++++++++ src/kernel/schedule/zephyr_ll_user.c | 70 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index 05e91427cda5..ae0661dd2b9b 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -234,6 +234,34 @@ enum zephyr_ll_karea { ZEPHYR_LL_KAREA_COUNT, }; +/** + * @brief Allocate from a kernel-managed area of the LL user memory region. + * + * For objects that kernel code creates and LL user threads access. The + * allocator state is kernel-only and memory is never freed, so use this + * for objects that live until the firmware is reset. Objects in + * ZEPHYR_LL_KAREA_RO can only be written by kernel threads, not by kernel + * code running in the LL memory domain, such as system calls from LL + * threads. Kernel context only. + * + * @param[in] area The area to allocate from. + * @param[in] flags SOF_MEM_FLAG_COHERENT for an uncached pointer. + * @param[in] bytes Size in bytes. + * @param[in] alignment Required alignment in bytes, 0 for the cache line size. + * @return Pointer to the memory, or NULL if the area is full. + */ +void *zephyr_ll_karea_alloc(enum zephyr_ll_karea area, uint32_t flags, size_t bytes, + size_t alignment); + +/** + * @brief Check that memory lies in a kernel-managed area of the LL region. + * @param[in] area The area to check. + * @param[in] ptr Start of the memory, cached or uncached alias. + * @param[in] bytes Size in bytes. + * @return true if [ptr, ptr + bytes) lies in the area. + */ +bool zephyr_ll_karea_contains(enum zephyr_ll_karea area, const void *ptr, size_t bytes); + /** * @brief Grant thread access to per-core Low-Latency mutexes. * @param[in,out] thread Target thread. diff --git a/src/kernel/schedule/zephyr_ll_user.c b/src/kernel/schedule/zephyr_ll_user.c index 9422023c355b..9c701928c7ca 100644 --- a/src/kernel/schedule/zephyr_ll_user.c +++ b/src/kernel/schedule/zephyr_ll_user.c @@ -6,8 +6,11 @@ #include #include +#include +#include #include +#include #include LOG_MODULE_DECLARE(ll_schedule, CONFIG_SOF_LOG_LEVEL); @@ -120,6 +123,73 @@ static void ll_region_init(void) (size_t)LL_HEAP_SIZE); } +/* serialises allocations from the kernel-managed areas */ +static struct k_spinlock ll_karea_lock; + +void *zephyr_ll_karea_alloc(enum zephyr_ll_karea area, uint32_t flags, size_t bytes, + size_t alignment) +{ + struct zephyr_ll_karea_state *ka; + k_spinlock_key_t key; + uintptr_t start; + void *ptr = NULL; + + __ASSERT(!k_is_user_context(), "kernel context only"); + + if (area >= ZEPHYR_LL_KAREA_COUNT || !bytes) + return NULL; + + ka = &ll_mem_resources.karea[area]; + + /* never share a cache line between objects with different aliases */ + alignment = MAX(alignment, PLATFORM_DCACHE_ALIGN); + /* area bases are page aligned, so offsets keep alignments up to a page */ + if (!is_power_of_2(alignment) || alignment > CONFIG_MM_DRV_PAGE_SIZE || + bytes > ka->size) + return NULL; + bytes = ROUND_UP(bytes, PLATFORM_DCACHE_ALIGN); + + key = k_spin_lock(&ll_karea_lock); + + /* offsets from the area base, all at most ka->size, cannot overflow */ + start = ROUND_UP(ka->used, alignment); + if (start <= ka->size && bytes <= ka->size - start) { + ka->used = start + bytes; + ptr = ka->base + start; + } + + k_spin_unlock(&ll_karea_lock, key); + + if (!ptr) { + tr_err(&ll_tr, "kernel area %d full: %zu bytes requested, %zu of %zu used", + area, bytes, ka->used, ka->size); + return NULL; + } + + tr_dbg(&ll_tr, "kernel area %d: %zu bytes at %p, %zu of %zu used", area, bytes, ptr, + ka->used, ka->size); + + if (!(flags & SOF_MEM_FLAG_COHERENT)) + ptr = (__sparse_force void *)sys_cache_cached_ptr_get(ptr); + + return ptr; +} + +bool zephyr_ll_karea_contains(enum zephyr_ll_karea area, const void *ptr, size_t bytes) +{ + const struct zephyr_ll_karea_state *ka; + uintptr_t start; + + if (area >= ZEPHYR_LL_KAREA_COUNT) + return false; + + ka = &ll_mem_resources.karea[area]; + start = (uintptr_t)sys_cache_uncached_ptr_get((__sparse_force void __sparse_cache *)ptr); + + return start >= (uintptr_t)ka->base && bytes <= ka->size && + start - (uintptr_t)ka->base <= ka->size - bytes; +} + void zephyr_ll_user_resources_init(void) { int ret; From bf837817101c9ab694cd409c8885c551cd52c3e5 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 11 Nov 2026 13:38:29 +0200 Subject: [PATCH 3/5] zephyr: alloc: add an option to log LL heap use by kernel threads Kernel code is to stop allocating from the LL user heap, whose metadata user threads can write. Add CONFIG_SOF_ZEPHYR_LL_HEAP_KERNEL_TRACE to log each allocation from and free to that heap made by a kernel thread, with size and caller address, for finding the remaining callers on target. Calls by user threads, including those that enter the kernel through system calls, are not logged. Signed-off-by: Kai Vehmanen --- zephyr/Kconfig | 10 ++++++++++ zephyr/lib/alloc.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/zephyr/Kconfig b/zephyr/Kconfig index 8408996c68bd..d8aa70793b56 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -203,6 +203,16 @@ config SOF_ZEPHYR_LL_KAREA_RO_SIZE SOF_ZEPHYR_SYS_USER_HEAP_SIZE. Must be a multiple of the MMU page size. +config SOF_ZEPHYR_LL_HEAP_KERNEL_TRACE + bool "Log LL user heap allocations made by kernel threads" + depends on SOF_USERSPACE_LL + help + Log every allocation from and free to the LL user heap that a + kernel thread makes, with its size and caller address. Kernel + code is being moved off this heap, whose metadata user threads + can write. Use this to find the remaining callers. Calls made by + user threads, including through system calls, are not logged. + config SOF_USERSPACE_PROXY bool "Use userspace proxy to support userspace modules" select SOF_USERSPACE_USE_DRIVER_HEAP diff --git a/zephyr/lib/alloc.c b/zephyr/lib/alloc.c index 89bd49286831..d44b4d6d1280 100644 --- a/zephyr/lib/alloc.c +++ b/zephyr/lib/alloc.c @@ -781,8 +781,41 @@ EXPORT_SYMBOL(rfree); * To match the fall-back SOF main heap all private heaps should also be in the * uncached address range. */ +#if CONFIG_SOF_ZEPHYR_LL_HEAP_KERNEL_TRACE +/* Log use of the LL user heap by kernel threads, see the Kconfig help */ +static void ll_heap_kernel_trace(const char *op, struct k_heap *heap, const void *ptr, + size_t bytes, const void *caller) +{ + struct k_thread *thread = k_current_get(); + + if (heap != zephyr_ll_user_heap() || (thread->base.user_options & K_USER)) + return; + + tr_warn(&zephyr_tr, "LL heap %s by kernel thread %s: %p %zu bytes, caller %p", op, + k_thread_name_get(thread) ? k_thread_name_get(thread) : "?", ptr, bytes, caller); +} +#else +static inline void ll_heap_kernel_trace(const char *op, struct k_heap *heap, const void *ptr, + size_t bytes, const void *caller) +{ +} +#endif + +static void *sof_heap_alloc_impl(struct k_heap *heap, uint32_t flags, size_t bytes, + size_t alignment); + void *z_impl_sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, size_t alignment) +{ + void *ptr = sof_heap_alloc_impl(heap, flags, bytes, alignment); + + ll_heap_kernel_trace("alloc", heap, ptr, bytes, __builtin_return_address(0)); + + return ptr; +} + +static void *sof_heap_alloc_impl(struct k_heap *heap, uint32_t flags, size_t bytes, + size_t alignment) { #if CONFIG_SOF_USERSPACE_USE_SHARED_HEAP /* @@ -824,6 +857,8 @@ void *z_impl_sof_heap_alloc(struct k_heap *heap, uint32_t flags, size_t bytes, void z_impl_sof_heap_free(struct k_heap *heap, void *addr) { + ll_heap_kernel_trace("free", heap, addr, 0, __builtin_return_address(0)); + if (heap && addr && is_heap_pointer(heap, addr)) heap_free(heap, addr); else From 38b381fff2185f5afc5d188e41b95c36c0897693 Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 11 Nov 2026 13:49:59 +0200 Subject: [PATCH 4/5] zephyr: test: userspace: add tests for the kernel-managed LL areas Check that an LL user thread can write objects allocated from the kernel RW area, can read objects in the kernel RO area and faults when writing them, and that the area bounds checks and oversized requests behave as expected. Signed-off-by: Kai Vehmanen --- zephyr/test/CMakeLists.txt | 1 + zephyr/test/userspace/test_ll_karea.c | 160 ++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 zephyr/test/userspace/test_ll_karea.c diff --git a/zephyr/test/CMakeLists.txt b/zephyr/test/CMakeLists.txt index e64b61314da4..07ae24b12bc6 100644 --- a/zephyr/test/CMakeLists.txt +++ b/zephyr/test/CMakeLists.txt @@ -31,6 +31,7 @@ endif() if(CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_LL) zephyr_library_sources(userspace/test_ll_task.c) + zephyr_library_sources(userspace/test_ll_karea.c) endif() if(CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_SYSCALL_CALLER_CHECK AND diff --git a/zephyr/test/userspace/test_ll_karea.c b/zephyr/test/userspace/test_ll_karea.c new file mode 100644 index 000000000000..3fe36bd4458d --- /dev/null +++ b/zephyr/test/userspace/test_ll_karea.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright(c) 2026 Intel Corporation. + */ + +/* + * Tests for the kernel-managed areas of the LL memory region: LL user + * threads can write objects in the RW area, only read objects in the RO + * area, and the kernel allocator state is not in user memory. + */ + +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG); + +#define USER_STACKSIZE 2048 +#define TEST_WORDS 4 +#define TEST_PATTERN 0x5ec0de42 + +static struct k_thread user_thread; +static K_THREAD_STACK_DEFINE(user_stack, USER_STACKSIZE); + +/* kernel-only, checked to be outside the areas */ +static uint32_t kernel_only_word; + +static uint32_t *rw_obj; +static uint32_t *ro_obj; + +enum karea_test_case { + /* write an object in the RW area */ + KAREA_WRITE_RW, + /* read an object in the RO area */ + KAREA_READ_RO, + /* write an object in the RO area */ + KAREA_WRITE_RO, +}; + +static void user_function(void *p1, void *p2, void *p3) +{ + enum karea_test_case test_case = (enum karea_test_case)(uintptr_t)p1; + uint32_t *obj = p2; + int i; + + zassert_true(k_is_user_context(), "isn't user"); + + switch (test_case) { + case KAREA_WRITE_RW: + for (i = 0; i < TEST_WORDS; i++) + obj[i] = TEST_PATTERN + i; + return; + case KAREA_READ_RO: + for (i = 0; i < TEST_WORDS; i++) + zassert_equal(obj[i], TEST_PATTERN + i); + return; + case KAREA_WRITE_RO: + obj[0] = 0; + break; + default: + zassert_unreachable("unknown test case"); + } + + zassert_unreachable("write to the RO area did not fault"); +} + +static void run_case(enum karea_test_case test_case, uint32_t *obj, bool expect_fault) +{ + int ret; + + k_thread_create(&user_thread, user_stack, USER_STACKSIZE, + user_function, (void *)(uintptr_t)test_case, obj, NULL, + -1, K_USER, K_FOREVER); + k_mem_domain_add_thread(zephyr_ll_mem_domain(), &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_boot_test_set_fault_valid(NULL); + + zassert_equal(ret, 0, "user thread join failed: %d", ret); +} + +static void *ll_karea_setup(void) +{ + /* the areas never free, so allocate the test objects once */ + rw_obj = zephyr_ll_karea_alloc(ZEPHYR_LL_KAREA_RW, 0, TEST_WORDS * sizeof(uint32_t), 0); + zassert_not_null(rw_obj); + ro_obj = zephyr_ll_karea_alloc(ZEPHYR_LL_KAREA_RO, 0, TEST_WORDS * sizeof(uint32_t), 0); + zassert_not_null(ro_obj); + + return NULL; +} + +ZTEST(userspace_ll_karea, rw_area_user_writable) +{ + int i; + + memset(rw_obj, 0, TEST_WORDS * sizeof(uint32_t)); + run_case(KAREA_WRITE_RW, rw_obj, false); + + for (i = 0; i < TEST_WORDS; i++) + zassert_equal(rw_obj[i], TEST_PATTERN + i); +} + +ZTEST(userspace_ll_karea, ro_area_user_readable) +{ + int i; + + /* kernel threads can write the RO area */ + for (i = 0; i < TEST_WORDS; i++) + ro_obj[i] = TEST_PATTERN + i; + + run_case(KAREA_READ_RO, ro_obj, false); +} + +ZTEST(userspace_ll_karea, ro_area_user_write_faults) +{ + ro_obj[0] = TEST_PATTERN; + run_case(KAREA_WRITE_RO, ro_obj, true); + zassert_equal(ro_obj[0], TEST_PATTERN, "RO object was written"); +} + +ZTEST(userspace_ll_karea, contains_and_limits) +{ + size_t sz = TEST_WORDS * sizeof(uint32_t); + + zassert_true(zephyr_ll_karea_contains(ZEPHYR_LL_KAREA_RW, rw_obj, sz)); + zassert_true(zephyr_ll_karea_contains(ZEPHYR_LL_KAREA_RO, ro_obj, sz)); + zassert_false(zephyr_ll_karea_contains(ZEPHYR_LL_KAREA_RW, ro_obj, sz)); + zassert_false(zephyr_ll_karea_contains(ZEPHYR_LL_KAREA_RO, rw_obj, sz)); + zassert_false(zephyr_ll_karea_contains(ZEPHYR_LL_KAREA_RW, &kernel_only_word, + sizeof(kernel_only_word))); + + /* a request larger than the area fails without using it up */ + zassert_is_null(zephyr_ll_karea_alloc(ZEPHYR_LL_KAREA_RW, 0, + CONFIG_SOF_ZEPHYR_LL_KAREA_RW_SIZE + 1, 0)); + zassert_is_null(zephyr_ll_karea_alloc(ZEPHYR_LL_KAREA_RO, 0, SIZE_MAX, 0)); +} + +ZTEST_SUITE(userspace_ll_karea, sof_boot_test_standalone_predicate, ll_karea_setup, + 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_ll_karea, false, 1, 1, NULL); + return 0; +} + +SYS_INIT(run_tests, APPLICATION, 99); From 6dfcc277ec3777367aa8b3a5511366c70dc0edfd Mon Sep 17 00:00:00 2001 From: Kai Vehmanen Date: Wed, 11 Nov 2026 13:50:14 +0200 Subject: [PATCH 5/5] userspace: doc: document the LL memory region layout Describe the kernel RO and RW areas of the LL memory region, their allocator and the restriction on writing the RO area, and the option for tracing LL heap use by kernel threads. Signed-off-by: Kai Vehmanen --- src/include/sof/userspace/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/include/sof/userspace/README.md b/src/include/sof/userspace/README.md index d8efa662aa2d..9631f346a89b 100644 --- a/src/include/sof/userspace/README.md +++ b/src/include/sof/userspace/README.md @@ -155,6 +155,28 @@ The `zephyr/syscall/*.c` files hold the validators in one place per subsystem; t Most syscall declarations are gated by `#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION)`; outside that guard (non-Zephyr host-test and ztest builds) the same name is a plain macro or inline calling `z_impl_*` directly. Non-userspace Zephyr builds (no `CONFIG_USERSPACE`) still use the `__syscall` declaration path, which Zephyr compiles to a direct `z_impl_*` call without the privilege-elevation trap. +## LL memory region + +With `CONFIG_SOF_USERSPACE_LL`, the memory of the LL and IPC user threads is one region of +`CONFIG_SOF_ZEPHYR_SYS_USER_HEAP_SIZE` bytes, allocated from the kernel heap at boot +(`src/kernel/schedule/zephyr_ll_user.c`) and split into page-aligned parts: + +| Part | Mapping in the LL memory domain | Allocator | +|------|---------------------------------|-----------| +| Kernel RO area (`CONFIG_SOF_ZEPHYR_LL_KAREA_RO_SIZE`) | read-only | `zephyr_ll_karea_alloc(ZEPHYR_LL_KAREA_RO, ...)` | +| Kernel RW area (`CONFIG_SOF_ZEPHYR_LL_KAREA_RW_SIZE`) | read-write | `zephyr_ll_karea_alloc(ZEPHYR_LL_KAREA_RW, ...)` | +| LL user heap (the rest) | read-write | `sof_heap_alloc(sof_sys_user_heap_get(), ...)` | + +The kernel areas are for objects that kernel code creates and LL user threads access. Their +allocator is a bump allocator with kernel-only state and no free, so use it for objects that +live until the firmware is reset. The Xtensa MMU has one write permission for both rings, so +the RO area is also read-only to kernel code running in the LL memory domain, such as system +calls from LL threads: only kernel threads can write objects there. + +The LL user heap keeps its `sys_heap` metadata in user-writable memory. Kernel code is being +moved off it; `CONFIG_SOF_ZEPHYR_LL_HEAP_KERNEL_TRACE` logs the remaining uses by kernel +threads. + ## Memory-partition markers User threads can only touch memory that is mapped into their memory domain. SOF exposes shared