Part 1
요약·해설
__syscall prototype에서 generated wrapper·unmarshaller·z_vrfy verifier·z_impl implementation으로 이어지는 전체 경로와 안전한 copy policy를 설명합니다.
prototype · wrapper · ID
object · memory · bounds
copy · TOCTOU · policy
Part 2
접을 수 있는 영어 원문 전체
영어 원문 전체 펼치기
79DA72378D5840E69B7D3E71132BB084D8D20479B1B6D10778980B344A62AC50
.. _syscalls:
System Calls
############
User threads run with a reduced set of privileges than supervisor threads:
certain CPU instructions may not be used, and they have access to only a
limited part of the memory map. System calls (may) allow user threads to
perform operations not directly available to them.
When defining system calls, it is very important to ensure that access to the
API's private data is done exclusively through system call interfaces.
Private kernel data should never be made available to user mode threads
directly. For example, the ``k_queue`` APIs were intentionally not made
available as they store bookkeeping information about the queue directly
in the queue buffers which are visible from user mode.
APIs that allow the user to register callback functions that run in
supervisor mode should never be exposed as system calls. Reserve these
for supervisor-mode access only.
This section describes how to declare new system calls and discusses a few
implementation details relevant to them.
Components
**********
All system calls have the following components:
* A **C prototype** prefixed with :c:macro:`__syscall` for the API. It
will be declared in some header under ``include/`` or in another
``SYSCALL_INCLUDE_DIRS`` directory. This prototype is never implemented
manually, instead it gets created by the :ref:`gen_syscalls.py` script.
What gets generated is an inline function which either calls the
implementation function directly (if called from supervisor mode) or goes
through privilege elevation and validation steps (if called from user
mode).
* An **implementation function**, which is the real implementation of the
system call. The implementation function may assume that all parameters
passed in have been validated if it was invoked from user mode.
* A **verification function**, which wraps the implementation function
and does validation of all the arguments passed in.
* An **unmarshalling function**, which is an automatically generated
handler that must be included by user source code.
C Prototype
***********
The C prototype represents how the API is invoked from either user or
supervisor mode. For example, to initialize a semaphore:
.. code-block:: c
__syscall void k_sem_init(struct k_sem *sem, unsigned int initial_count,
unsigned int limit);
The :c:macro:`__syscall` attribute is very special. To the C compiler, it
simply expands to 'static inline'. However to the post-build
:ref:`parse_syscalls.py` script, it indicates that this API is a system call.
The :ref:`parse_syscalls.py` script does some parsing of the function prototype,
to determine the data types of its return value and arguments, and has some
limitations:
* Array arguments must be passed in as pointers, not arrays. For example,
``int foo[]`` or ``int foo[12]`` is not allowed, but should instead be
expressed as ``int *foo``.
* Function pointers horribly confuse the limited parser. The workaround is
to typedef them first, and then express in the argument list in terms
of that typedef.
* :c:macro:`__syscall` must be the first thing in the prototype.
The preprocessor is intentionally not used when determining the set of system
calls to generate. However, any generated system calls that don't actually have
a verification function defined (because the related feature is not enabled in
the kernel configuration) will instead point to a special verification for
unimplemented system calls. Data type definitions for APIs should not have
conditional visibility to the compiler.
Any header file that declares system calls must include a special generated
header at the very bottom of the header file. This header follows the
naming convention ``syscalls/<name of header file>``. For example, at the
bottom of ``include/sensor.h``:
.. code-block:: c
#include <zephyr/syscalls/sensor.h>
C prototype functions must be declared in one of the directories
listed in the CMake variable ``SYSCALL_INCLUDE_DIRS``. This list
always contains ``APPLICATION_SOURCE_DIR`` when
``CONFIG_APPLICATION_DEFINED_SYSCALL`` is set, or
``${ZEPHYR_BASE}/subsys/testsuite/ztest/include`` when
``CONFIG_ZTEST`` is set. Additional paths can be added to the list
through the CMake command line or in CMake code that is run before
``find_package(Zephyr ...)`` is run. ``${ZEPHYR_BASE}/include``
is always scanned for potential syscall prototypes.
Note that not all syscalls will be included in the final binaries.
CMake functions ``zephyr_syscall_header`` and
``zephyr_syscall_header_ifdef`` are used to specify which header
files contain syscall prototypes where those syscalls must be
present in the final binaries. Note that header files inside
directories listed in CMake variable ``SYSCALL_INCLUDE_DIRS``
will always have their syscalls present in final binaries.
To force all syscalls to be included in the final binaries,
turn on :kconfig:option:`CONFIG_EMIT_ALL_SYSCALLS`.
Invocation Context
==================
Source code that uses system call APIs can be made more efficient if it is
known that all the code inside a particular C file runs exclusively in
user mode, or exclusively in supervisor mode. The system will look for
the definition of macros :c:macro:`__ZEPHYR_SUPERVISOR__` or
:c:macro:`__ZEPHYR_USER__`, typically these will be added to the compiler
flags in the build system for the related files.
* If :kconfig:option:`CONFIG_USERSPACE` is not enabled, all APIs just directly call
the implementation function.
* Otherwise, the default case is to make a runtime check to see if the
processor is currently running in user mode, and either make the system call
or directly call the implementation function as appropriate.
* If :c:macro:`__ZEPHYR_SUPERVISOR__` is defined, then it is assumed that
all the code runs in supervisor mode and all APIs just directly call the
implementation function. If the code was actually running in user mode,
there will be a CPU exception as soon as it tries to do something it isn't
allowed to do.
* If :c:macro:`__ZEPHYR_USER__` is defined, then it is assumed that all the
code runs in user mode and system calls are unconditionally made.
Implementation Details
======================
Declaring an API with :c:macro:`__syscall` causes some code to be generated in
C and header files by the :ref:`gen_syscalls.py` script, all of which can be found in
the project out directory under ``include/generated/``:
* The system call is added to the enumerated type of system call IDs,
which is expressed in ``include/generated/zephyr/syscall_list.h``. It is the name
of the API in uppercase, prefixed with ``K_SYSCALL_``.
* An entry for the system call is created in the dispatch table
``_k_syscall_table``, expressed in ``include/generated/zephyr/syscall_dispatch.c``
* This table only contains syscalls where their corresponding
prototypes are declared in header files when
:kconfig:option:`CONFIG_EMIT_ALL_SYSCALLS` is enabled:
* Indicated by CMake functions ``zephyr_syscall_header`` and
``zephyr_syscall_header_ifdef``, or
* Under directories specified in CMake variable
``SYSCALL_INCLUDE_DIRS``.
* A weak verification function is declared, which is just an alias of the
'unimplemented system call' verifier. This is necessary since the real
verification function may or may not be built depending on the kernel
configuration. For example, if a user thread makes a sensor subsystem
API call, but the sensor subsystem is not enabled, the weak verifier
will be invoked instead.
* An unmarshalling function is defined in ``include/generated/<name>_mrsh.c``
The body of the API is created in the generated system header. Using the
example of :c:func:`k_sem_init()`, this API is declared in
``include/kernel.h``. At the bottom of ``include/kernel.h`` is::
#include <zephyr/syscalls/kernel.h>
Inside this header is the body of :c:func:`k_sem_init()`::
static inline void k_sem_init(struct k_sem * sem, unsigned int initial_count, unsigned int limit)
{
#ifdef CONFIG_USERSPACE
if (z_syscall_trap()) {
arch_syscall_invoke3(*(uintptr_t *)&sem, *(uintptr_t *)&initial_count, *(uintptr_t *)&limit, K_SYSCALL_K_SEM_INIT);
return;
}
compiler_barrier();
#endif
z_impl_k_sem_init(sem, initial_count, limit);
}
This generates an inline function that takes three arguments with void
return value. Depending on context it will either directly call the
implementation function or go through a system call elevation. A
prototype for the implementation function is also automatically generated.
The final layer is the invocation of the system call itself. All architectures
implementing system calls must implement the seven inline functions
:c:func:`_arch_syscall_invoke0` through :c:func:`_arch_syscall_invoke6`. These
functions marshal arguments into designated CPU registers and perform the
necessary privilege elevation. Parameters of API inline function, before being
passed as arguments to system call, are C casted to ``uintptr_t`` which matches
size of register.
Exception to above is passing 64-bit parameters on 32-bit systems, in which case
64-bit parameters are split into lower and higher part and passed as two consecutive
arguments.
There is always a ``uintptr_t`` type return value, which may be neglected if
not needed.
.. figure:: ../../../../kernel/usermode/syscall_flow.png
:alt: System Call execution flow
:width: 80%
:align: center
System Call execution flow
Some system calls may have more than six arguments, but number of arguments
passed via registers is limited to six for all architectures.
Additional arguments will need to be passed in an array in the source memory
space, which needs to be treated as untrusted memory in the verification
function. This code (packing, unpacking and validation) is generated
automatically as needed in the stub above and in the unmarshalling function.
System calls return ``uintptr_t`` type value that is C casted, by wrapper, to
a return type of API prototype declaration. This means that 64-bit value may
not be directly returned, from a system call to its wrapper, on 32-bit systems.
To solve the problem the automatically generated wrapper function defines 64-bit
intermediate variable, which is considered **untrusted** buffer, on its stack
and passes pointer to that variable to the system call, as a final argument.
Upon return from the system call the value written to that buffer will be
returned by the wrapper function.
The problem does not exist on 64-bit systems which are able to return 64-bit
values directly.
Implementation Function
***********************
The implementation function is what actually does the work for the API.
Zephyr normally does little to no error checking of arguments, or does this
kind of checking with assertions. When writing the implementation function,
validation of any parameters is optional and should be done with assertions.
All implementation functions must follow the naming convention, which is the
name of the API prefixed with ``z_impl_``. Implementation functions may be
declared in the same header as the API as a static inline function or
declared in some C file. There is no prototype needed for implementation
functions, these are automatically generated.
Verification Function
*********************
The verification function runs on the kernel side when a user thread makes
a system call. When the user thread makes a software interrupt to elevate to
supervisor mode, the common system call entry point uses the system call ID
provided by the user to look up the appropriate unmarshalling function for that
system call and jump into it. This in turn calls the verification function.
Verification and unmarshalling functions only run when system call APIs are
invoked from user mode. If an API is invoked from supervisor mode, the
implementation is simply called and there is no software trap.
The purpose of the verification function is to validate all the arguments
passed in. This includes:
* Any kernel object pointers provided. For example, the semaphore APIs must
ensure that the semaphore object passed in is a valid semaphore and that
the calling thread has permission on it.
* Any memory buffers passed in from user mode. Checks must be made that the
calling thread has read or write permissions on the provided buffer.
* Any other arguments that have a limited range of valid values.
Verification functions involve a great deal of boilerplate code which has been
made simpler by some macros in :zephyr_file:`include/zephyr/internal/syscall_handler.h`.
Verification functions should be declared using these macros.
Argument Validation
===================
Several macros exist to validate arguments:
* :c:macro:`K_SYSCALL_OBJ()` Checks a memory address to assert that it is
a valid kernel object of the expected type, that the calling thread
has permissions on it, and that the object is initialized.
* :c:macro:`K_SYSCALL_OBJ_INIT()` is the same as
:c:macro:`K_SYSCALL_OBJ()`, except that the provided object may be
uninitialized. This is useful for verifiers of object init functions.
* :c:macro:`K_SYSCALL_OBJ_NEVER_INIT()` is the same as
:c:macro:`K_SYSCALL_OBJ()`, except that the provided object must be
uninitialized. This is not used very often, currently only for
:c:func:`k_thread_create()`.
* :c:macro:`K_SYSCALL_MEMORY_READ()` validates a memory buffer of a particular
size. The calling thread must have read permissions on the entire buffer.
* :c:macro:`K_SYSCALL_MEMORY_WRITE()` is the same as
:c:macro:`K_SYSCALL_MEMORY_READ()` but the calling thread must additionally
have write permissions.
* :c:macro:`K_SYSCALL_MEMORY_ARRAY_READ()` validates an array whose total size
is expressed as separate arguments for the number of elements and the
element size. This macro correctly accounts for multiplication overflow
when computing the total size. The calling thread must have read permissions
on the total size.
* :c:macro:`K_SYSCALL_MEMORY_ARRAY_WRITE()` is the same as
:c:macro:`K_SYSCALL_MEMORY_ARRAY_READ()` but the calling thread must
additionally have write permissions.
* :c:macro:`K_SYSCALL_VERIFY_MSG()` does a runtime check of some boolean
expression which must evaluate to true otherwise the check will fail.
A variant :c:macro:`K_SYSCALL_VERIFY` exists which does not take
a message parameter, instead printing the expression tested if it
fails. The latter should only be used for the most obvious of tests.
* :c:macro:`K_SYSCALL_DRIVER_OP()` checks at runtime if a driver
instance is capable of performing a particular operation. While this
macro can be used by itself, it's mostly a building block for macros
that are automatically generated for every driver subsystem. For
instance, to validate the GPIO driver, one could use the
:c:macro:`K_SYSCALL_DRIVER_GPIO()` macro.
* :c:macro:`K_SYSCALL_SPECIFIC_DRIVER()` is a runtime check to verify that
a provided pointer is a valid instance of a specific device driver, that
the calling thread has permissions on it, and that the driver has been
initialized. It does this by checking the API structure pointer that
is stored within the driver instance and ensuring that it matches the
provided value, which should be the address of the specific driver's
API structure.
If any check fails, the macros will return a nonzero value. The macro
:c:macro:`K_OOPS()` can be used to induce a kernel oops which will kill the
calling thread. This is done instead of returning some error condition to
keep the APIs the same when calling from supervisor mode.
.. _syscall_verification:
Verifier Definition
===================
All system calls are dispatched to a verifier function with a prefixed
``z_vrfy_`` name based on the system call. They have exactly the same
return type and argument types as the wrapped system call. Their job
is to execute the system call (generally by calling the implementation
function) after having validated all arguments.
The verifier is itself invoked by an automatically generated
unmarshaller function which takes care of unpacking the register
arguments from the architecture layer and casting them to the correct
type. This is defined in a header file that must be included from
user code, generally somewhere after the definition of the verifier in
a translation unit (so that it can be inlined).
For example:
.. code-block:: c
static int z_vrfy_k_sem_take(struct k_sem *sem, int32_t timeout)
{
K_OOPS(K_SYSCALL_OBJ(sem, K_OBJ_SEM));
return z_impl_k_sem_take(sem, timeout);
}
#include <zephyr/syscalls/k_sem_take_mrsh.c>
Verification Memory Access Policies
===================================
Parameters passed to system calls by reference require special handling,
because the value of these parameters can be changed at any time by any
user thread that has access to the memory that parameter points to. If the
kernel makes any logical decisions based on the contents of this memory, this
can open up the kernel to attacks even if checking is done. This is a class
of exploits known as TOCTOU (Time Of Check to Time Of Use).
The proper procedure to mitigate these attacks is to make a copies in the
verification function, and only perform parameter checks on the copies, which
user threads will never have access to. The implementation functions get passed
the copy and not the original data sent by the user. The
:c:func:`k_usermode_to_copy()` and :c:func:`k_usermode_from_copy()` APIs exist for
this purpose.
There is one exception in place, with respect to large data buffers which are
only used to provide a memory area that is either only written to, or whose
contents are never used for any validation or control flow. Further
discussion of this later in this section.
As a first example, consider a parameter which is used as an output parameter
for some integral value:
.. code-block:: c
int z_vrfy_some_syscall(int *out_param)
{
int local_out_param;
int ret;
ret = z_impl_some_syscall(&local_out_param);
K_OOPS(k_usermode_to_copy(out_param, &local_out_param, sizeof(*out_param)));
return ret;
}
Here we have allocated ``local_out_param`` on the stack, passed its address to
the implementation function, and then used :c:func:`k_usermode_to_copy()` to fill
in the memory passed in by the caller.
It might be tempting to do something more concise:
.. code-block:: c
int z_vrfy_some_syscall(int *out_param)
{
K_OOPS(K_SYSCALL_MEMORY_WRITE(out_param, sizeof(*out_param)));
return z_impl_some_syscall(out_param);
}
However, this is unsafe if the implementation ever does any reads to this
memory as part of its logic. For example, it could be used to store some
counter value, and this could be meddled with by user threads that have access
to its memory. It is by far safest for small integral values to do the copying
as shown in the first example.
Some parameters may be input/output. For instance, it's not uncommon to see APIs
which pass in a pointer to some ``size_t`` which is a maximum allowable size,
which is then updated by the implementation to reflect the actual number of
bytes processed. This too should use a stack copy:
.. code-block:: c
int z_vrfy_in_out_syscall(size_t *size_ptr)
{
size_t size;
int ret;
K_OOPS(k_usermode_from_copy(&size, size_ptr, sizeof(size));
ret = z_impl_in_out_syscall(&size);
K_OOPS(k_usermode_to_copy(size_ptr, &size, sizeof(size)));
return ret;
}
Many system calls pass in structures or even linked data structures. All should
be copied. Typically this is done by allocating copies on the stack:
.. code-block:: c
struct bar {
...
};
struct foo {
...
struct bar *bar_left;
struct bar *bar_right;
};
int z_vrfy_must_alloc(struct foo *foo)
{
int ret;
struct foo foo_copy;
struct bar bar_right_copy;
struct bar bar_left_copy;
K_OOPS(k_usermode_from_copy(&foo_copy, foo, sizeof(*foo)));
K_OOPS(k_usermode_from_copy(&bar_right_copy, foo_copy.bar_right,
sizeof(struct bar)));
foo_copy.bar_right = &bar_right_copy;
K_OOPS(k_usermode_from_copy(&bar_left_copy, foo_copy.bar_left,
sizeof(struct bar)));
foo_copy.bar_left = &bar_left_copy;
return z_impl_must_alloc(&foo_copy);
}
In some cases the amount of data isn't known at compile time or may be too
large to allocate on the stack. In this scenario, it may be necessary to draw
memory from the caller's resource pool via :c:func:`z_thread_malloc()`. This
should always be considered last resort. Functional safety programming
guidelines heavily discourage usage of heap and the fact that a resource pool is
used must be clearly documented. Any issues with allocation must be
reported, to a caller, with returning the ``-ENOMEM`` . The ``K_OOPS()``
should never be used to verify if resource allocation has been successful.
.. code-block:: c
struct bar {
...
};
struct foo {
size_t count;
struct bar *bar_list; /* array of struct bar of size count */
};
int z_vrfy_must_alloc(struct foo *foo)
{
int ret;
struct foo foo_copy;
struct bar *bar_list_copy;
size_t bar_list_bytes;
/* Safely copy foo into foo_copy */
K_OOPS(k_usermode_from_copy(&foo_copy, foo, sizeof(*foo)));
/* Bounds check the count member, in the copy we made */
if (foo_copy.count > 32) {
return -EINVAL;
}
/* Allocate RAM for the bar_list, replace the pointer in
* foo_copy */
bar_list_bytes = foo_copy.count * sizeof(struct_bar);
bar_list_copy = z_thread_malloc(bar_list_bytes);
if (bar_list_copy == NULL) {
return -ENOMEM;
}
K_OOPS(k_usermode_from_copy(bar_list_copy, foo_copy.bar_list,
bar_list_bytes));
foo_copy.bar_list = bar_list_copy;
ret = z_impl_must_alloc(&foo_copy);
/* All done with the memory, free it and return */
k_free(foo_copy.bar_list_copy);
return ret;
}
Finally, we must consider large data buffers. These represent areas of user
memory which either have data copied out of, or copied into. It is permitted
to pass these pointers to the implementation function directly. The caller's
access to the buffer still must be validated with ``K_SYSCALL_MEMORY`` APIs.
The following constraints need to be met:
* If the buffer is used by the implementation function to write data, such
as data captured from some MMIO region, the implementation function must
only write this data, and never read it.
* If the buffer is used by the implementation function to read data, such
as a block of memory to write to some hardware destination, this data
must be read without any processing. No conditional logic can be implemented
due to the data buffer's contents. If such logic is required a copy must be
made.
* The buffer must only be used synchronously with the call. The implementation
must not ever save the buffer address and use it asynchronously, such as
when an interrupt fires.
.. code-block:: c
int z_vrfy_get_data_from_kernel(void *buf, size_t size)
{
K_OOPS(K_SYSCALL_MEMORY_WRITE(buf, size));
return z_impl_get_data_from_kernel(buf, size);
}
Verification Return Value Policies
==================================
When verifying system calls, it's important to note which kinds of verification
failures should propagate a return value to the caller, and which should
simply invoke :c:macro:`K_OOPS()` which kills the calling thread. The current
conventions are as follows:
#. For system calls that are defined but not compiled, invocations of these
missing system calls are routed to :c:func:`handler_no_syscall()` which
invokes :c:macro:`K_OOPS()`.
#. Any invalid access to memory found by the set of ``K_SYSCALL_MEMORY`` APIs,
:c:func:`k_usermode_from_copy()`, :c:func:`k_usermode_to_copy()`
should trigger a :c:macro:`K_OOPS`. This happens when the caller doesn't have
appropriate permissions on the memory buffer or some size calculation
overflowed.
#. Most system calls take kernel object pointers as an argument, checked either
with one of the ``K_SYSCALL_OBJ`` functions, ``K_SYSCALL_DRIVER_nnnnn``, or
manually using :c:func:`k_object_validate()`. These can fail for a variety
of reasons: missing driver API, bad kernel object pointer, wrong kernel
object type, or improper initialization state. These issues should always
invoke :c:macro:`K_OOPS()`.
#. Any error resulting from a failed memory heap allocation, often from
invoking :c:func:`z_thread_malloc()`, should propagate ``-ENOMEM`` to the
caller.
#. General parameter checks should be done in the implementation function,
in most cases using ``CHECKIF()``.
* The behavior of ``CHECKIF()`` depends on the kernel configuration, but if
user mode is enabled, :kconfig:option:`CONFIG_RUNTIME_ERROR_CHECKS` is enforced,
which guarantees that these checks will be made and a return value
propagated.
#. It is totally forbidden for any kind of kernel mode callback function to
be registered from user mode. APIs which simply install callbacks shall not
be exposed as system calls. Some driver subsystem APIs may take optional
function callback pointers. User mode verification functions for these APIs
must enforce that these are NULL and should invoke :c:macro:`K_OOPS()` if
not.
#. Some parameter checks are enforced only from user mode. These should be
checked in the verification function and propagate a return value to the
caller if possible.
There are some known exceptions to these policies currently in Zephyr:
* :c:func:`k_thread_join()` and :c:func:`k_thread_abort()` are no-ops if
the thread object isn't initialized. This is because for threads, the
initialization bit pulls double-duty to indicate whether a thread is
running, cleared upon exit. See #23030.
* :c:func:`k_thread_create()` invokes :c:macro:`K_OOPS()` for parameter
checks, due to a great deal of existing code ignoring the return value.
This will also be addressed by #23030.
* :c:func:`k_thread_abort()` invokes :c:macro:`K_OOPS()` if an essential
thread is aborted, as the function has no return value.
* Various system calls related to logging invoke :c:macro:`K_OOPS()`
when bad parameters are passed in as they do not propagate errors.
Configuration Options
*********************
Related configuration options:
* :kconfig:option:`CONFIG_USERSPACE`
* :kconfig:option:`CONFIG_EMIT_ALL_SYSCALLS`
APIs
****
Helper macros for creating system call verification functions are provided in
:zephyr_file:`include/zephyr/internal/syscall_handler.h`:
* :c:macro:`K_SYSCALL_OBJ()`
* :c:macro:`K_SYSCALL_OBJ_INIT()`
* :c:macro:`K_SYSCALL_OBJ_NEVER_INIT()`
* :c:macro:`K_OOPS()`
* :c:macro:`K_SYSCALL_MEMORY_READ()`
* :c:macro:`K_SYSCALL_MEMORY_WRITE()`
* :c:macro:`K_SYSCALL_MEMORY_ARRAY_READ()`
* :c:macro:`K_SYSCALL_MEMORY_ARRAY_WRITE()`
* :c:macro:`K_SYSCALL_VERIFY_MSG()`
* :c:macro:`K_SYSCALL_VERIFY`
Functions for invoking system calls are defined in
:zephyr_file:`include/zephyr/syscall.h`:
* :c:func:`_arch_syscall_invoke0`
* :c:func:`_arch_syscall_invoke1`
* :c:func:`_arch_syscall_invoke2`
* :c:func:`_arch_syscall_invoke3`
* :c:func:`_arch_syscall_invoke4`
* :c:func:`_arch_syscall_invoke5`
* :c:func:`_arch_syscall_invoke6`
Part 3
한국어 전문 번역
User thread는 supervisor thread보다 낮은 privilege로 실행됩니다. 일부 CPU instruction을 사용할 수 없고 memory map의 제한된 부분만 접근합니다. System call은 user thread가 직접 수행할 수 없는 operation을 수행하도록 허용할 수 있습니다.
이 절은 새 system call을 선언하는 방법과 관련 implementation detail을 설명합니다.
구성요소
모든 system call은 다음 네 부분으로 구성됩니다.
- C prototype: API prototype 앞에
__syscall을 붙입니다.include/아래나 다른SYSCALL_INCLUDE_DIRSdirectory에 선언합니다. 직접 구현하지 않고gen_syscalls.py가 생성합니다. 결과는 supervisor mode이면 implementation function을 직접 부르고, user mode이면 privilege elevation과 validation을 거치는 inline function입니다. - Implementation function: System call의 실제 동작입니다. User mode에서 호출됐을 때 전달 parameter는 이미 검증됐다고 가정할 수 있습니다.
- Verification function: Implementation을 감싸고 전달된 모든 argument를 검증합니다.
- Unmarshalling function: 자동 생성되는 handler이며 user source code에서 include해야 합니다.
C Prototype
C prototype은 user mode와 supervisor mode 양쪽에서 API를 호출하는 형태를 나타냅니다. Semaphore initialization 예시는 다음과 같습니다.
__syscall void k_sem_init(struct k_sem *sem, unsigned int initial_count,
unsigned int limit);__syscall attribute는 C compiler에는 단순히 static inline으로 확장되지만 post-build parse_syscalls.py에는 이 API가 system call임을 알립니다. 이 script는 function prototype을 parse해 return value와 argument data type을 판단하므로 다음 제한이 있습니다.
- Array argument는 array가 아니라 pointer로 전달해야 합니다.
int foo[]와int foo[12]는 허용되지 않고int *foo로 표현해야 합니다. - 제한적인 parser는 function pointer를 제대로 해석하지 못합니다. 먼저 typedef를 만들고 argument list에서는 그 typedef를 사용해야 합니다.
__syscall은 prototype의 맨 앞에 있어야 합니다.
생성할 system-call 집합을 정할 때 preprocessor는 의도적으로 사용하지 않습니다. 관련 feature가 kernel configuration에서 꺼져 verification function이 실제로 정의되지 않은 system call은 unimplemented-system-call용 special verifier를 가리킵니다. API data-type definition은 compiler에 조건부로 보이면 안 됩니다.
System call을 선언한 모든 header file은 맨 아래에서 syscalls/<header file 이름> 규칙의 특별한 generated header를 include해야 합니다. 예를 들어 include/sensor.h의 끝은 다음과 같습니다.
#include <zephyr/syscalls/sensor.h>C prototype은 CMake variable SYSCALL_INCLUDE_DIRS에 나열된 directory 중 하나에 있어야 합니다. CONFIG_APPLICATION_DEFINED_SYSCALL이면 APPLICATION_SOURCE_DIR, CONFIG_ZTEST이면 ${ZEPHYR_BASE}/subsys/testsuite/ztest/include가 항상 포함됩니다. find_package(Zephyr ...) 실행 전 CMake command line이나 CMake code로 path를 더할 수 있고, ${ZEPHYR_BASE}/include는 항상 potential prototype을 scan합니다.
모든 syscall이 final binary에 포함되는 것은 아닙니다. zephyr_syscall_header와 zephyr_syscall_header_ifdef CMake function으로 반드시 포함할 syscall prototype header를 지정합니다. 다만 SYSCALL_INCLUDE_DIRS 아래 header의 syscall은 항상 final binary에 들어갑니다. 모든 syscall을 강제로 넣으려면 CONFIG_EMIT_ALL_SYSCALLS를 켭니다.
Invocation Context
특정 C file의 code가 전부 user mode 또는 전부 supervisor mode에서만 실행된다는 것을 알면 system-call API 호출을 더 효율적으로 만들 수 있습니다. Build system은 보통 compiler flag로 __ZEPHYR_SUPERVISOR__ 또는 __ZEPHYR_USER__ macro를 정의합니다.
CONFIG_USERSPACE가 꺼져 있으면 모든 API가 implementation function을 직접 호출합니다.- 그 외 default는 processor가 현재 user mode인지 runtime에 확인해 system call 또는 direct implementation call을 선택합니다.
__ZEPHYR_SUPERVISOR__가 정의되면 모든 code가 supervisor mode라고 가정해 implementation을 직접 호출합니다. 실제로 user mode였다면 허용되지 않은 operation을 시도하는 즉시 CPU exception이 납니다.__ZEPHYR_USER__가 정의되면 모든 code가 user mode라고 가정해 조건 없이 system call을 수행합니다.
Implementation Details
__syscall로 API를 선언하면 gen_syscalls.py가 project output의 include/generated/ 아래에 C·header code를 만듭니다.
- System-call ID enum인
include/generated/zephyr/syscall_list.h에 API 이름을 uppercase로 바꾸고K_SYSCALL_prefix를 붙인 항목이 추가됩니다. include/generated/zephyr/syscall_dispatch.c의_k_syscall_tabledispatch table에 항목이 만들어집니다.CONFIG_EMIT_ALL_SYSCALLS일 때 이 table에는 CMake의zephyr_syscall_header·zephyr_syscall_header_ifdef가 가리키거나SYSCALL_INCLUDE_DIRS아래 header에 선언된 syscall만 들어갑니다.- Weak verification function을 선언하고 unimplemented-system-call verifier의 alias로 둡니다. 실제 verifier는 kernel configuration에 따라 build되지 않을 수 있습니다. 예를 들어 sensor subsystem이 꺼진 상태에서 user thread가 sensor API를 호출하면 weak verifier가 실행됩니다.
include/generated/<name>_mrsh.c에 unmarshalling function을 정의합니다.
API body는 generated system header에 만들어집니다. k_sem_init()은 include/kernel.h에 선언되고 그 끝에서 다음 header를 include합니다.
#include <zephyr/syscalls/kernel.h>이 generated header 안의 k_sem_init() body는 다음과 같습니다.
static inline void k_sem_init(struct k_sem * sem, unsigned int initial_count, unsigned int limit)
{
#ifdef CONFIG_USERSPACE
if (z_syscall_trap()) {
arch_syscall_invoke3(*(uintptr_t *)&sem, *(uintptr_t *)&initial_count, *(uintptr_t *)&limit, K_SYSCALL_K_SEM_INIT);
return;
}
compiler_barrier();
#endif
z_impl_k_sem_init(sem, initial_count, limit);
}이 inline function은 argument 3개와 void return을 가지며 invocation context에 따라 implementation을 직접 호출하거나 system-call privilege elevation을 거칩니다. Implementation function prototype도 자동 생성됩니다.
마지막 layer는 system call 자체의 invocation입니다. System call을 지원하는 모든 architecture는 _arch_syscall_invoke0부터 _arch_syscall_invoke6까지 7개 inline function을 구현해야 합니다. 이들은 argument를 지정 CPU register에 marshal하고 필요한 privilege elevation을 수행합니다. API parameter는 register size와 같은 uintptr_t로 C cast한 뒤 전달합니다. 32-bit system의 64-bit parameter만 예외로 low·high part로 나눠 연속 argument 두 개로 보냅니다. Return은 항상 uintptr_t이며 필요 없으면 무시할 수 있습니다.
__syscall generated inline
supervisor → z_impl direct / user → trap
0~6 register arguments · privilege elevation
register와 extra array unpack
object · memory · parameter validation
z_impl_* 실제 동작
uintptr_t cast · user mode 복귀
6개를 넘는 argument와 32-bit system의 64-bit return에는 untrusted user-memory 보조 buffer가 사용됩니다.
System call이 6개보다 많은 argument를 가질 수 있지만 모든 architecture에서 register로 전달하는 수는 최대 6개입니다. 나머지는 source memory-space의 array로 전달하며 verification function에서 untrusted memory로 취급해야 합니다. Stub과 unmarshalling function의 packing·unpacking·validation code는 필요에 따라 자동 생성됩니다.
System call의 uintptr_t return은 wrapper가 API prototype의 return type으로 cast합니다. 그래서 32-bit system에서는 64-bit value를 wrapper로 직접 반환할 수 없습니다. 자동 생성 wrapper는 stack에 untrusted buffer로 취급되는 64-bit intermediate variable을 만들고 그 pointer를 마지막 argument로 system call에 넘깁니다. System call이 buffer에 쓴 값을 wrapper가 반환합니다. 64-bit value를 직접 반환할 수 있는 64-bit system에는 이 문제가 없습니다.
Implementation Function
Implementation function은 API의 실제 일을 수행합니다. Zephyr는 보통 argument error checking을 거의 하지 않거나 assertion으로 검사합니다. Implementation을 작성할 때 parameter validation은 선택 사항이며 필요하면 assertion을 사용해야 합니다.
모든 implementation function은 API 이름 앞에 z_impl_을 붙여야 합니다. API와 같은 header에서 static inline으로 선언하거나 C file에서 선언할 수 있습니다. Prototype은 자동 생성되므로 별도로 만들 필요가 없습니다.
Verification Function
User thread가 system call을 하면 verification function이 kernel side에서 실행됩니다. User thread가 software interrupt로 supervisor mode로 올라가면 common system-call entry point가 user가 제공한 system-call ID로 알맞은 unmarshalling function을 찾아 이동하고, 그 function이 verifier를 호출합니다.
Verification과 unmarshalling function은 user mode에서 API를 호출할 때만 실행됩니다. Supervisor mode 호출은 software trap 없이 implementation을 바로 부릅니다.
Verifier의 목적은 모든 argument를 검증하는 것입니다.
- Kernel-object pointer가 expected type의 valid object인지, caller에게 permission이 있는지, 올바르게 initialized됐는지 확인합니다.
- User mode에서 받은 memory buffer 전체에 caller가 필요한 read 또는 write permission을 가졌는지 확인합니다.
- 유효한 값 범위가 제한된 다른 argument를 확인합니다.
Verification function에는 boilerplate가 많으므로 include/zephyr/internal/syscall_handler.h의 macro를 사용해 선언합니다.
Argument Validation
K_SYSCALL_OBJ(): address가 expected type의 valid kernel object이고 caller permission이 있으며 initialized됐는지 검사합니다.K_SYSCALL_OBJ_INIT(): 앞 macro와 같지만 object가 uninitialized여도 됩니다. Object-init function verifier에 유용합니다.K_SYSCALL_OBJ_NEVER_INIT(): object가 반드시 uninitialized여야 합니다. 현재는 주로k_thread_create()에만 사용합니다.K_SYSCALL_MEMORY_READ(): 지정 size의 memory buffer 전체에 caller read permission이 있는지 검사합니다.K_SYSCALL_MEMORY_WRITE(): read 조건에 더해 caller write permission도 요구합니다.K_SYSCALL_MEMORY_ARRAY_READ(): element count와 element size로 total size를 계산하는 array를 검증하며 multiplication overflow도 정확히 처리합니다. 전체에 read permission이 필요합니다.K_SYSCALL_MEMORY_ARRAY_WRITE(): array-read 조건에 write permission을 추가합니다.K_SYSCALL_VERIFY_MSG(): boolean expression이 true인지 runtime에 검사하고 실패 message를 받습니다. Message 없이 tested expression을 출력하는K_SYSCALL_VERIFYvariant는 아주 명백한 검사에만 사용해야 합니다.K_SYSCALL_DRIVER_OP(): driver instance가 특정 operation을 수행할 수 있는지 runtime에 검사합니다. 단독 사용도 가능하지만 각 driver subsystem용 자동 생성 macro의 building block입니다. GPIO는K_SYSCALL_DRIVER_GPIO()를 사용할 수 있습니다.K_SYSCALL_SPECIFIC_DRIVER(): pointer가 특정 device-driver instance이고 caller permission이 있으며 initialized됐는지 검사합니다. Driver instance의 API-structure pointer가 제공한 specific-driver API structure address와 같은지 비교합니다.
검사가 실패하면 macro는 nonzero를 반환합니다. K_OOPS()로 kernel oops를 일으켜 caller thread를 종료할 수 있습니다. Supervisor-mode direct call과 API 형태를 같게 유지하기 위해 error condition을 반환하는 대신 이 방식을 사용합니다.
Verifier Definition
모든 system call은 API 이름 앞에 z_vrfy_를 붙인 verifier function으로 dispatch됩니다. Return type과 argument type은 감싼 system call과 정확히 같으며, 모든 argument를 검증한 다음 보통 implementation function을 호출해 system call을 실행합니다.
Verifier는 architecture layer의 register argument를 풀어 올바른 type으로 cast하는 자동 생성 unmarshaller가 호출합니다. 이 unmarshaller header는 user code의 translation unit에서 verifier definition 뒤에 include해 inline될 수 있게 하는 것이 일반적입니다.
static int z_vrfy_k_sem_take(struct k_sem *sem, int32_t timeout)
{
K_OOPS(K_SYSCALL_OBJ(sem, K_OBJ_SEM));
return z_impl_k_sem_take(sem, timeout);
}
#include <zephyr/syscalls/k_sem_take_mrsh.c>Verification Memory Access Policies
Reference로 전달된 parameter는 그 memory에 접근할 수 있는 user thread가 언제든 값을 바꿀 수 있어 특별히 다뤄야 합니다. Kernel이 이 memory content로 logical decision을 하면 사전에 검사했어도 공격 가능하며, 이를 TOCTOU(Time Of Check to Time Of Use) exploit이라고 합니다.
방어하려면 verification function에서 private copy를 만들고 user thread가 접근하지 못하는 copy만 검사해야 합니다. Implementation에도 user original이 아니라 copy를 넘깁니다. 이를 위해 k_usermode_to_copy()와 k_usermode_from_copy()를 제공합니다.
Integral output parameter의 안전한 예시는 local stack variable을 implementation에 넘긴 뒤 결과를 user memory로 copy하는 방식입니다.
int z_vrfy_some_syscall(int *out_param)
{
int local_out_param;
int ret;
ret = z_impl_some_syscall(&local_out_param);
K_OOPS(k_usermode_to_copy(out_param, &local_out_param, sizeof(*out_param)));
return ret;
}local_out_param을 stack에 만들고 implementation에 address를 넘긴 뒤 k_usermode_to_copy()로 caller가 제공한 memory를 채웁니다.
다음처럼 user buffer의 write permission만 검사하고 implementation에 직접 넘기는 간결한 방식은 안전하지 않습니다.
int z_vrfy_some_syscall(int *out_param)
{
K_OOPS(K_SYSCALL_MEMORY_WRITE(out_param, sizeof(*out_param)));
return z_impl_some_syscall(out_param);
}Implementation logic가 이 memory를 한 번이라도 읽으면 user thread가 값을 바꿔 counter 같은 내부 상태를 교란할 수 있습니다. 작은 integral value는 첫 예시처럼 copy하는 것이 가장 안전합니다.
Input/output parameter도 stack copy를 사용해야 합니다. 예를 들어 maximum size가 든 size_t *를 받고 실제 처리 byte 수로 갱신하는 API는 다음과 같이 양방향 copy합니다.
int z_vrfy_in_out_syscall(size_t *size_ptr)
{
size_t size;
int ret;
K_OOPS(k_usermode_from_copy(&size, size_ptr, sizeof(size));
ret = z_impl_in_out_syscall(&size);
K_OOPS(k_usermode_to_copy(size_ptr, &size, sizeof(size)));
return ret;
}Structure나 linked data structure를 받는 system call도 모두 copy해야 하며 보통 stack에 각 node의 copy를 할당합니다.
struct bar {
...
};
struct foo {
...
struct bar *bar_left;
struct bar *bar_right;
};
int z_vrfy_must_alloc(struct foo *foo)
{
int ret;
struct foo foo_copy;
struct bar bar_right_copy;
struct bar bar_left_copy;
K_OOPS(k_usermode_from_copy(&foo_copy, foo, sizeof(*foo)));
K_OOPS(k_usermode_from_copy(&bar_right_copy, foo_copy.bar_right,
sizeof(struct bar)));
foo_copy.bar_right = &bar_right_copy;
K_OOPS(k_usermode_from_copy(&bar_left_copy, foo_copy.bar_left,
sizeof(struct bar)));
foo_copy.bar_left = &bar_left_copy;
return z_impl_must_alloc(&foo_copy);
}Data size를 compile time에 모르거나 stack에 두기 너무 크면 caller resource pool에서 z_thread_malloc()으로 memory를 얻어야 할 수 있습니다. 이는 마지막 수단이어야 합니다. Functional-safety guideline은 heap 사용을 강하게 권장하지 않으며 resource pool 사용 사실을 명확히 문서화해야 합니다. Allocation failure는 caller에 -ENOMEM을 반환해야 하고 allocation success 검증에 K_OOPS()를 절대 사용하면 안 됩니다.
struct bar {
...
};
struct foo {
size_t count;
struct bar *bar_list; /* array of struct bar of size count */
};
int z_vrfy_must_alloc(struct foo *foo)
{
int ret;
struct foo foo_copy;
struct bar *bar_list_copy;
size_t bar_list_bytes;
/* Safely copy foo into foo_copy */
K_OOPS(k_usermode_from_copy(&foo_copy, foo, sizeof(*foo)));
/* Bounds check the count member, in the copy we made */
if (foo_copy.count > 32) {
return -EINVAL;
}
/* Allocate RAM for the bar_list, replace the pointer in
* foo_copy */
bar_list_bytes = foo_copy.count * sizeof(struct_bar);
bar_list_copy = z_thread_malloc(bar_list_bytes);
if (bar_list_copy == NULL) {
return -ENOMEM;
}
K_OOPS(k_usermode_from_copy(bar_list_copy, foo_copy.bar_list,
bar_list_bytes));
foo_copy.bar_list = bar_list_copy;
ret = z_impl_must_alloc(&foo_copy);
/* All done with the memory, free it and return */
k_free(foo_copy.bar_list_copy);
return ret;
}마지막으로 user memory에서 data를 읽어 내거나 써 넣는 큰 data buffer는 pointer를 implementation에 직접 넘길 수 있습니다. 그래도 K_SYSCALL_MEMORY API로 caller access를 검증하고 다음 조건을 모두 지켜야 합니다.
- Implementation이 MMIO에서 capture한 data처럼 buffer에 쓰는 경우 write만 해야 하며 절대로 읽으면 안 됩니다.
- Hardware destination으로 보낼 block처럼 buffer를 읽는 경우 processing 없이 읽어야 합니다. Buffer content에 따른 conditional logic이 필요하면 copy를 만들어야 합니다.
- Buffer는 call과 동기적으로만 사용해야 합니다. Interrupt가 나중에 쓸 수 있도록 address를 저장하는 등 asynchronous 사용은 금지됩니다.
int z_vrfy_get_data_from_kernel(void *buf, size_t size)
{
K_OOPS(K_SYSCALL_MEMORY_WRITE(buf, size));
return z_impl_get_data_from_kernel(buf, size);
}Verification Return Value Policies
검증 실패를 caller return value로 전파할지, K_OOPS()로 caller thread를 종료할지는 다음 convention을 따릅니다.
- 정의됐지만 compile되지 않은 system call은
handler_no_syscall()로 route되고K_OOPS()를 호출합니다. K_SYSCALL_MEMORYAPI,k_usermode_from_copy(),k_usermode_to_copy()가 찾은 invalid memory access는K_OOPS()를 일으켜야 합니다. Caller permission이 없거나 size calculation이 overflow한 경우입니다.K_SYSCALL_OBJ,K_SYSCALL_DRIVER_nnnnn, 또는k_object_validate()가 찾은 missing driver API, bad object pointer, wrong type, improper initialization state는 항상K_OOPS()를 호출해야 합니다.z_thread_malloc()같은 heap allocation 실패는 caller에-ENOMEM을 반환해야 합니다.- 일반 parameter check는 implementation function에서, 대부분
CHECKIF()로 수행합니다. 동작은 kernel configuration에 따라 다르지만 user mode가 켜지면CONFIG_RUNTIME_ERROR_CHECKS가 강제돼 check와 return-value propagation을 보장합니다. - User mode가 kernel-mode callback을 등록하는 것은 완전히 금지됩니다. Callback 설치 API는 system call로 노출하면 안 되고, optional callback pointer를 받는 driver API의 user verifier는 NULL만 허용하며 아니면
K_OOPS()를 호출해야 합니다. - User mode에서만 적용하는 일부 parameter check는 verifier에서 검사하고 가능하면 return value를 caller에 전파합니다.
현재 Zephyr에는 다음 예외가 알려져 있습니다.
k_thread_join()과k_thread_abort()는 thread object가 initialized되지 않았으면 아무 일도 하지 않습니다. Thread에서는 initialization bit가 실행 중 여부도 나타내며 exit 때 clear되기 때문입니다. Issue #23030에서 다룹니다.k_thread_create()는 기존 code가 return value를 무시하는 경우가 많아 parameter check에K_OOPS()를 사용합니다. 이것도 #23030 대상입니다.k_thread_abort()는 return value가 없으므로 essential thread를 abort하려 하면K_OOPS()를 호출합니다.- Logging 관련 여러 system call은 error를 전파하지 않으므로 bad parameter에
K_OOPS()를 사용합니다.
Configuration Options
CONFIG_USERSPACECONFIG_EMIT_ALL_SYSCALLS
APIs
System-call verification function 작성용 helper macro는 include/zephyr/internal/syscall_handler.h에 있습니다.
K_SYSCALL_OBJ()K_SYSCALL_OBJ_INIT()K_SYSCALL_OBJ_NEVER_INIT()K_OOPS()K_SYSCALL_MEMORY_READ()K_SYSCALL_MEMORY_WRITE()K_SYSCALL_MEMORY_ARRAY_READ()K_SYSCALL_MEMORY_ARRAY_WRITE()K_SYSCALL_VERIFY_MSG()K_SYSCALL_VERIFY
System call을 invoke하는 function은 include/zephyr/syscall.h에 정의됩니다.
_arch_syscall_invoke0_arch_syscall_invoke1_arch_syscall_invoke2_arch_syscall_invoke3_arch_syscall_invoke4_arch_syscall_invoke5_arch_syscall_invoke6
Source
출처
영어 원문에는 문단, 문서 지시문, 표, 코드, 코드 심벌, 경로를 그대로 보존했습니다.