doc: syscalls: expand docs on data copying
Show best practices when handling parameters passed to system calls by reference. Signed-off-by: Andrew Boie <andrew.p.boie@intel.com>
This commit is contained in:
parent
68a2fe8262
commit
cc680a83dd
1 changed files with 192 additions and 2 deletions
|
@ -332,8 +332,198 @@ For example:
|
|||
}
|
||||
#include <syscalls/k_sem_take_mrsh.c>
|
||||
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue