This page provides a list of common coding patterns and best practices specific to Starnix development.
Starnix, which is essentially a kernel running in userspace, runs Linux programs on Fuchsia. This creates specific considerations for developers who need to ensure that their code produces the results expected by Linux programs. This page aims to clarify some of these patterns and provide best practices, covering topics like testing, managing userspace addresses, handling error messages, and more.
The topics are:
- Testing Starnix using Linux binaries
- Representing userspace addresses in Starnix
- Handling syscall arguments in Starnix
- Safe error handling in Starnix
- Creating errors in Starnix
- Preventing arithmetic overflow in Starnix
Testing Starnix using Linux binaries
This section covers best practices for testing Starnix functionality, which involves using binaries compiled for Linux.
Most of Starnix's test coverage comes from userspace binaries compiled for Linux. The Fuchsia project runs these binaries on both Linux and Starnix to make sure that Starnix matches Linux behavior.
Userspace unit tests verify the Linux UAPI, which is what Starnix implements. Verifying Starnix behavior at this level gives us the freedom to refactor Starnix's implementation with confidence.
In addition, Starnix kernel unit tests can be useful for verifying internal invariants of the system. However, if you write kernel unit tests, be careful to avoid "change-detector tests." In other words, ensure that you don't write tests that fail when the implementation changes even if the changes are functionally correct.
Representing userspace addresses in Starnix
This section covers best practices for representing and validating addresses within the userspace of Starnix.
The UserAddress
and UserRef
types are used to denote addresses that are in
"Linux userspace" in Starnix (that is, the
restricted address space). Once it is determined that
a UserAddress
points to an object of type T
, convert it to a UserRef<T>
.
This approach provides more type information, which makes it easier to read
and write to and from userspace.
Address validation is performed by Fuchsia's memory management subsystem. In general, code outside of the memory manager should not perform any checks to determine whether or not an address is valid before passing the address to the memory manager (for example, checking that an address is non-null).
Good
pub fn sys_something(current_task: &CurrentTask, user_events: UserRef<epoll_event>)
-> Result <(), Errno> {
let events = current_task.read_object(user_events)?;
...
}
Bad
pub fn sys_something(current_task: &CurrentTask, user_events: UserRef<epoll_event>)
-> Result <(), Errno> {
if user_events.addr().is_null() {
return error!(EFAULT);
}
let events = current_task.read_object(user_events)?;
...
}
However, the most common exception to this rule is when a syscall needs to return a specific error when a provided address is null, for example:
pub fn sys_something(current_task: &CurrentTask, user_events: UserRef<epoll_event>)
-> Result <(), Errno> {
if user_events.addr().is_null() {
// The memory manager would never return ENOSYS for a read_object at a null
// address, so an explicit check is required.
return error!(ENOSYS);
}
let events = current_task.read_object(user_events)?;
...
}
Handling syscall arguments in Starnix
This section focuses on best practices around SyscallArg
, which is the
default type for all Starnix syscall arguments.
All arguments to Starnix syscall implementations start out as SyscallArg
. This
type is then converted into specific syscall argument types using the into()
trait. For instance, when a syscall is being dispatched, it will be called as:
match syscall_nr {
__NR_execve => {
sys_execve(arg0.into(), arg1.into(), arg3.into())
}
}
This gives the syscall implementation flexibility to use any type that
SyscallArg
can be converted into.
Good
fn sys_execve(
user_path: UserCString,
user_argv: UserRef<UserCString>,
user_environ: UserRef<UserCString>,
) -> Result<(), Errno> {
...
}
Bad
fn sys_execve(
user_path: SyscallArg,
user_argv: SyscallArg,
user_environ: SyscallArg,
) -> Result<(), Errno> {
...
}
Safe error handling in Starnix
This section discusses the risks of using the unwrap()
and expect()
methods
within Starnix.
Panicking in Starnix is the equivalent of a kernel panic for the container it is
running. This means that if a syscall uses APIs like unwrap()
or expect()
,
it has the potential to panic, not only the process that caused the error, but
the entire container.
Good
let value = option.ok_or_else(|| error!(EINVAL))?;
This example is good practice because it uses ok_or_else()
to handle
error cases.
Bad
let value = option.unwrap();
This example is bad practice because it has the potential to panic and bring down the entire container, not just the process that caused the invariant to be violated.
However, if an error is truly unrecoverable, it is acceptable to use unwrap()
or expect()
. However, its use should contain a context string that describes
why a kernel panic is the only option.
Creating errors in Starnix
This section provides best practices for creating and translating errors.
Starnix uses a wrapper type for Linux error codes called errno
. This type is
useful because it can capture the source location of the error, which is helpful
when debugging. When creating new errors, use the error!()
macro.
Good
if !name.entry.node.is_dir() {
return error!(ENOTDIR, "Invalid path provided to sys_chroot");
}
This example is good practice because the errno!()
macro provides more
information, which helps debugging.
Bad
return error!(EINVAL);
This example is bad practice because it returns an error code without any context or information about the error.
Plus, when translating one error into another, it may be convenient to use
map_err()
with the errno!()
macro.
Good
let s = mm.read_c_string_to_vec(user_string, elem_limit).map_err(|e| {
if e == errno!(ENAMETOOLONG) {
errno!(E2BIG)
} else {
e
}
})?;
Bad
let s = match mm.read_c_string_to_vec(user_string, elem_limit) {
Err(e) if e == errno!(ENAMETOOLONG) => {
errno!(E2BIG)
},
Err(e) => {
e
},
ok => ok,
}?;
Preventing arithmetic overflow in Starnix
This section emphasizes the importance of using checked math operations when dealing with numerical values originating from userspace.
Always use checked math (for example, checked_add()
and checked_mul()
) with
numerical values that come from userspace. This prevents bad values in userspace
from overflowing arithmetic in the kernel.
Good
pub fn sys_something(user_value: u32) -> Result<(), Errno> {
let value = user_value.get()?;
let result = value.checked_mul(2).ok_or_else(|| error!(EOVERFLOW))?;
...
}
This example is good practice because the code uses checked_mul()
to
perform multiplication on a value retrieved from userspace. This ensures that if
the multiplication overflows, an EOVERFLOW
error is returned instead of
potentially causing unexpected behavior or crashes.
Bad
pub fn sys_something(user_value: u32) -> Result<(), Errno> {
let value = user_value.get()?;
let result = value * 2; // Potential overflow here!
...
}
This example is bad practice because the code directly multiplies the user-supplied value by 2 without checking for potential overflow. If the value is large enough, the multiplication could overflow, leading to incorrect results or a system crash.