NASA lays out a set of rules in the paper “The Power of 10 Rules” that, when followed, help increase confidence in the correctness of software. This aids the development of safety-critical software systems by reducing ambiguity and complexity as much as possible. The rules are designed to make the software easier to understand, analyse, and verify safety-critical applications where failures can have severe consequences.
goto statements, setjmp or
longjmp constructs, or direct or indirect recursion.typedef
declarations. Function pointers are never permitted.When safty-critical software fails, real world consequences are realised. Some notable failures include:
Standards and certification processes for safety-critical software are crucial to ensure correctness and reliable operation. Domain-specific standards such as DO-178C for aerospace, IEC 62304 for medical devices, and ISO 26262 for automotive software provide comprehensive guidelines, rules, and design processes across a system’s lifecycle, including requirements traceability, software design, coding practices, testing, and verification. Adherence to these standards is mandatory before a system can be certified for use in safety-critical contexts. Rigorous documentation, testing, and independent verification are required to ensure that the software meets the necessary safety requirements.
An abstracted example of a software system that averages a sensor’s readings and reports the calculated value at a fixed interval can illustrate the importance of following an appropriate standard. When the system’s output is used for safety-critical decision making, it is vital to be able to verify the correctness of the software to increase confidence in the system’s ability to perform its intended function without failure. The following code snippets illustrate both a poor implementation in which little confidence can be placed, and an alternative implementation that increases confidence by adhering to the rules outlined above.
#include <stdio.h>
#include <stdlib.h>
int *samples = NULL;
int idx = 0;
void process_sensor(void) {
while (1) {
int v;
if (scanf("%d", &v) != 1) break;
if (!samples) samples = malloc(sizeof(int) * 1000);
samples[idx++] = v;
if (idx > 16) {
goto report;
}
}
report:
long sum = 0;
for (int i = 0; i < idx; ++i) sum += samples[i];
double avg = (double)sum / idx;
printf("Avg: %f\n", avg);
}
int main() {
process_sensor();
free(samples);
return 0;
}Here a goto statement is used to jump to the report
section, which makes the control flow harder to reason about. The
unbounded loop can lead to resource exhaustion if the input is large.
Dynamic memory allocation is used without a robust strategy for
deallocation, which can lead to memory leaks. The function is long and
includes multiple responsibilities, making it harder to understand and
verify. Variables are not declared at the smallest scope, and the index
is hidden and unchecked, which can lead to buffer overflows. There are
no assertions or checks for anomalous conditions; when runtime errors
occur the code lacks a clear recovery strategy. The return value of
scanf is not handled robustly, which can lead to incorrect
processing of input data.
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
#define SAMPLE_COUNT 16
static int sensor_read(void) {
int v;
if (scanf("%d", &v) != 1) return INT32_MIN;
return v;
}
#define CHECK(cond, action) do { if (!(cond)) { action; } } while (0)
int compute_average(const int buf[], size_t n, double *out) {
CHECK(buf != NULL, return -1);
CHECK(out != NULL, return -1);
CHECK(n > 0, return -2);
long sum = 0;
for (size_t i = 0; i < n; ++i) {
sum += buf[i];
}
*out = (double)sum / (double)n;
return 0;
}
int main(void) {
int samples[SAMPLE_COUNT];
size_t count = 0;
for (size_t i = 0; i < SAMPLE_COUNT; ++i) {
int v = sensor_read();
if (v == INT32_MIN) {
break;
}
samples[count++] = v;
}
double avg;
int rc = compute_average(samples, count == 0 ? 1 : count, &avg);
if (rc != 0) {
fprintf(stderr, "compute_average failed (rc=%d)\n", rc);
return 1;
}
printf("Average over %zu samples: %.3f\n", count, avg);
return 0;
}No goto statements are used, creating a clearer control
flow. A fixed upper bound on the number of iterations is established by
SAMPLE_COUNT, ensuring that the loop cannot exceed a preset
upper bound resulting in resource exhaustion. Dynamic memory allocation
is avoided after initialization by using a fixed-size array
samples. Functions are short and focused, with each having
a single responsibility. Assertions are implemented using the
CHECK macro, which checks for anomalous conditions and
takes explicit recovery actions by returning error codes. Variables are
declared at the smallest possible scope, and all return values are
checked to ensure that errors are handled appropriately. The use of the
preprocessor is limited to a simple macro definition for assertions, and
pointer use is simple and straightforward. The code is written to be
compiled with warnings enabled, supporting static analysis and ensuring
that it adheres to good coding practices.
When compiling this code, the most pedantic warnings should be enabled with:
gcc -Wall -Wextra -pedantic -o safe_avg safe_avg.c