apache / apache/nuttx

Remove errno accesses from the OS

Open
#1,027 7 comments 0 reactions 0 assignees View on GitHub
Area: Modularity Type: Enhancement
Dominant language
C
Stars
4k
Forks
1.7k
Avg merge
1d 17h
Merged PRs (30d)
237

Description

## Propsition

Nothing inside of the OS should access the per-thread `errno` variable. Why?

1. This is a improper separation of responsibilities. `errno` is strictly a feature of the user-space via `libs/libc`. The OS should perform basic operation, but user space operations under `libs/libc` should be the only place where the errno is maniuplated.

Linux and GLIBC have this same separation of functionality: GLIBC manages the `errno` variable, not Linux

2. Modifying the `errno` is dangerous within the OS. The `errno` is kept in TLS which resides on the thread's stack. The OS internals, however, may operate on different kernel threads or even, as Linux does, perform all system calls on a different stack. So, whenever a snippet of code within the OS modifies the `errno`, it is assuming implicitly that caller's stack is in effect. But that might not always be the case.

In the KERNEL mode of operation, the user stack may not be accessible unless the correct _address environment_ is instantiated, making modification of the user stack more complex, more risky.

So it would be a positive improvement to the OS if all `errno` operations were removed from the OS and, instead, performed in user space by logic in `libs/libc`

## Front End Functions

Within the OS, special measures have been taken so that the OS does not modify the `errno` in most cases. This is done with (1) a _front end, user OS interfaces_ that modify the `errno` and (2) a separate internal version of the OS interface that does not modify the `errno`. This latter interface is the one used exclusively within the OS.

The general pattern is like for the following for `some_os_interface()`:

int some_os_interface(int some_parameters)
{
int ret = nx_some_os_interface(some_parameters);
if (ret < 0)
{
set_errno(-ret);
ret = ERROR;
}

return ret;
}

Where `nx_some_os_interface()` is the internal implementation of the OS interface that does NOT set the `errno` variable.

This general pattern could be easily moved to user space in `/libs/libc.` Only a few things would have to change:

1. The implementation of `some_os_interface()` would have to be separated and moved to a new file under `/libs/libc`.
2. In` syscall/*` and in` include/sys/*`, all references to `some_os_interface()` would have to be changed to `nx_some_os_interface()`.

Easy, right? Only two things make this difficult: (1) there are many such tiny functions that would have to be changed, and (2) cancellation points make things more complex.

## Cancellation Points

If `some_os_interface()` is a cancellation point, then there is more in the front end function. Such a front end function would look more like:

int some_os_interface(int some_parameters)
{
int ret;

enter_cancellation_point();

ret = nx_some_os_interface(some_parameters);
if (ret < 0)
{
set_errno(-ret);
ret = ERROR;
}

leave_cancellation_point();
return ret;
}

And `enter/leave_cancellation_point()` are OS functions that cannot be called from user-space logic in `libs/libc`. In this case, `some_os_interface()` would have to be separated into two functions:

int some_cancellable_os_interface(int some_parameters)
{
int ret;

enter_cancellation_point();

ret = nx_some_os_interface(some_parameters);

leave_cancellation_point();
return ret;
}

And

int some_os_interface(int some_parameters)
{
int ret;

enter_cancellation_point();

ret = some_cancellable_os_interface(some_parameters);
if (ret < 0)
{
set_errno(-ret);
ret = ERROR;
}

leave_cancellation_point();
return ret;
}

1. The implementation of `some_os_interface()` would have to be separated into two functions and the one that modifies the `errno` would be moved to a new file under `/libs/libc`.
2. In `syscall/*` and in i`nclude/sys/*`, all references to `some_os_interface ()`would have to be changed to `some_cancelable_os_interface()`.

## Signal Handlers

I have not done any detailed analysis, but the other place where there are many modifications to errno value is in implementations of the function `up_sigdeliver()`. In that function, it preserves the old `errno` value, calls the signal handler, then restores the old `errno` value. This preserves the `errno` value so that it is not lost due to signal handling logic.

In this case, the signal handler should always be running on the same stack as the thread that it interrupted so `errno` access should be safe.

However, this logic could simply be removed: It is not the responsibility of the OS signal handling logic to preserve the `errno` value; it is the responsibility of the signal handler to save and restore the `errno` value. Per GLIBC documentation under "1.2.2.1 POSIX Safety Concepts" (https://www.gnu.org/software/libc/manual/html_node/POSIX-Safety-Concepts.html):

> "AS-Safe or Async-Signal-Safe functions are safe to call from asynchronous signal handlers. AS, in AS-Safe, stands for Asynchronous Signal.
>
> "Many functions that are AS-Safe may set errno, or modify the floating-point environment, because their doing so does not make them unsuitable for use in signal handlers. However, programs could misbehave should asynchronous signal handlers modify this thread-local state, and the signal handling machinery cannot be counted on to preserve it. Therefore, signal handlers that call functions that may set errno or modify the floating-point environment must save their original values, and restore them before returning."

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.