diff --git a/doc/reference/usermode/syscalls.rst b/doc/reference/usermode/syscalls.rst index 91221ab2a02..46014faead5 100644 --- a/doc/reference/usermode/syscalls.rst +++ b/doc/reference/usermode/syscalls.rst @@ -332,8 +332,198 @@ For example: } #include -Verification Policies -===================== + +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:`z_user_to_copy()` and :c:func:`z_user_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); + Z_OOPS(z_user_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:`z_user_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) + { + Z_OOPS(Z_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; + + Z_OOPS(z_user_from_copy(&size, size_ptr, sizeof(size)); + ret = z_impl_in_out_syscall(&size); + *size_ptr = size; + return ret; + } + +Many system calls pass in structs, 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; + + Z_OOPS(z_user_from_copy(&foo_copy, foo, sizeof(*foo))); + Z_OOPS(z_user_from_copy(&bar_right_copy, foo_copy.bar_right, + sizeof(struct bar))); + foo_copy.bar_right = &bar_right_copy; + Z_OOPS(z_user_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 a method of last resort. Functional safety programming +guidelines heavily discourge the use of heaps, the fact that a resource pool is +used must be clearly documented, and any issue with allocations must be +propaged to the caller with a ``-ENOMEM`` return value, never a ``Z_OOPS()``. + +.. 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 */ + Z_OOPS(z_user_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; + } + Z_OOPS(z_user_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 ``Z_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) + { + Z_OOPS(Z_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