Rubric for writing Starnix syscalls

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.