Processing arguments
Syscalls should process arguments in the order they are provided from userspace. For example, arguments should be validated in the order they are provided to the syscall. Validation order is especially important if different the syscall produces different Errno values for different validation errors because we need to return the correct Errno value to userspace if there are multiple ways in which the arguments are invalid.
If the syscall argument needs to read userspace memory (e.g., if the argument
is a UserAddress or a UserRef), the syscall should read userspace memory
using arguments in the order those arguments are provided. The order in which we
read from userspace memory is visible to userspace because those reads can
generate faults, which are reported to userspace.
Error handling
Userspace should not be able to use syscalls to crash the Starnix kernel. For
example, syscalls should not use Rust APIs that panic (e.g., unwrap or
expect) when they encounter invalid or unexpected parameters.
Flags or options
Many syscalls take a bitfield argument (often a u32) that carries the flags
or options for the syscall. A good practice is to validate this bitfield by
checking whether the argument contains an unknown bits before doing more
detailed processing of the argument.
Example:
if flags & !(AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW) != 0 {
track_stub!(...)
return error!(EINVAL);
}
Use the track_stub macro to track the unknown flag. Tracking unsupported
features helps people who are debugging userspace code notice that the issue
they are seeing might be caused by missing functionality in Starnix.
Starnix often uses the bitflags macro to define a Starnix-internal type for
bitfields passes as arguments to syscalls. In most cases, syscalls should use
the from_bits method to convert the raw syscall argument into the
Starnix-internal type because from_bits lets the caller explicitly handle the
case where the raw value contains unknown bits.
Arithmetic on user-space provided values
Syscalls should avoid using arithmetic on values provided from userspace that can cause numerical overflow. For example, if a syscall receives two numerical arguments from userspace, adding those values (without validation) can cause a numerical overflow if userspace provides excessively large values. In Rust, numerical overflow causes a panic, which is not the correct way of handling invalid arguments.
Instead, either validate the range of the numerical values or use a function
like checked_add, which lets the calling code handle overflow explicitly.
Consider using the UserValue instead of a raw numerical type to make it easier
to validate values from userspace.
User addresses
When working with userspace addresses, do not use raw numerical values or raw
pointers. Instead, use the UserAddress type for addresses. If the address is a
pointer to a specific struct in userspace, use the UserRef type, which has the
specific struct as a type parameter. If the address is a pointer to a struct
that has a different layout on different architectures, use the
MultiArchUserRef type. The UserRef and MultiArchUserRef types are also
useful for processing arrays of objects.
Syscalls should not read the same location from userspace memory more than once because another thread in userspace could modify userspace memory while the syscall is executing. Instead, copy from userspace into a variable inside the syscall implementation.
Syscalls that write to userspace memory need to be careful because userspace might provide addresses that overlap each other. For this reason, syscalls should not interleave reading and writing userspace memory. Instead, syscalls should perform all their reads from userspace before performing their writes.