Skip to content

RFC: kernel heap requests on behalf of syscalls #6972

@andrewboie

Description

@andrewboie

We've found some situations where we are going to need to allow the kernel side of system calls to reserve memory on some private kernel heap, for short or long-lived purposes, because user mode cannot be trusted to provide this memory itself. However, if we allow this, we must have some means to prevent user mode threads from DoS the kernel heap by requesting all of its memory.

Let's go through some examples where this is needed:

  1. Dynamic allocation of kernel objects, an RFC patch which exists in dynamic kernel objects #6876. We want to allow for kernel objects to be allocated dynamically, not just statically as we have now. It would be very desirable to allow user mode threads to also request kernel objects.
  2. We have some system calls which would like to do some deferred processing. A good example is the k_delayed_work APIs. In order to properly do deferred processing, you need to instantiate a struct _timeout which gets added to system timeout lists. Once the timeout expires, the _timeout can be deleted. The _timeout cannot be in user-accessible memory, so any deferred workqueue requests will need a transient heap allocation to handle the timeout.
  3. Using the workqueue example again, there's a larger struct k_work which is queued when work is requested and de-queued by the worker thread. A user mode compatible workqueue would need temporary allocation of k_work objects until the worker thread consumes them.
  4. In a more general case, we could introduce the notion of unbounded k_queues by allocating the linked list behind the scenes, right now the only IPC data exchange mechanism we allow user mode to use are pipes which are fixed size.
  5. k_pipes are themselves currently problematic. We really can't trust user mode to provide the buffer where the pipe data gets queued. If user mode wants to initialize a pipe object we need the pipe's buffer to be allocated on a heap instead.

So given all this, we still don't want threads to spam syscalls and eat up the entire kernel heap. There needs to be some limits on how much kernel heap data a user thread can indirectly consume on behalf of these calls. I can think of 3 methods for doing this:

  1. Limit number of heap requests. Simpler per-thread counter, any given thread cannot make more than N requests before the request fails, where N defaults to some Kconfig and can be adjusted by supervisor mode at runtime. Freeing memory will credit the thread that made the request (even if freed by some other thread or an ISR)
  2. Limit number of bytes of heap requests. Just like zephyr-master_fork #1, but account for actual number of bytes requested. Any given thread can only reserve N bytes.
  3. Don't do numerical accounting, instead there can be a supervisor mode to assign a k_mem_pool to a thread. Any requests made by that thread will draw from that memory pool. Defaults to NULL, so no requests allowed. Supervisor mode API to assign a pool to a thread. Multiple threads can all be using the same pool, child threads will inherit the pool assigned to the parent. If DoS is not a concern, just make this be the kernel heap.

I feel Option 1 doesn't account for divergent sizes of object requests, it would only work well if all requests were generally on the same order of magnitude in size, and I won't discuss Option 1 further.

Option 2 is simple and easy and doesn't require the creation of additional pools. However it means that child threads would again be able to reserve as much stuff as the parent; it doesn't impose any kind of cap on a group of threads. We don't (yet) have a process model in Zephyr, with multiple threads running in one process. Something like option 2 would be ideal later if when introduce processes, even if it's only an abstraction layer for non-MMU systems.

Option 3 is a bit more cumbersome since additional memory pools will have to be defined. But it does offer the ability to have threads that form some logical "application" on the device to all draw from a single pool. One of the more popular use-cases of Zephyr is to have multiple, more or less independent applications all running on the same MCU, APIs like Memory Domains and so forth are intended for this. So if each application has its own pool for syscall requests, then they can't DoS each other. The downside is that since these are separate pools, they all have to be set up, a proper size determined, threads assigned to them, etc.

I'm looking for input on approach:

  • Pursue Option 2 by introducing a Zephyr "process" abstraction layer and have the heap limits shared by all threads that belong to that process. This is what I am trending towards. The same process-level APIs will then tie into future work to introduce virtual memory.
  • Pursue Option 2 but disregard potential DoS scenarios where a ton of user threads are created and eat up the heap in concert
  • Pursue Option 3 as it would be fastest to implement even though not particularly easy to use.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FeatureA planned feature with a milestoneRFCRequest For Comments: want input from the communityarea: Memory Protection

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions