Aikido

セーフティ・クリティカルなコードに関するNasaの10のコーディング・ルール

はじめに

宇宙船や自動車システムで使用されるようなセーフティ・クリティカルなソフトウェアには、極めて信頼性の高いコードが必要です。これに対処するため、NASAのジェット推進研究所は2006年に
‍"Power of 10 "コーディングルールを作成した。この簡潔なガイドラインは、解析が困難な複雑なCの構成要素を取り除き、コードが単純で、検証可能で、信頼できる状態を維持することを保証するものである。

今日、Aikido コードクオリティのようなツールは、新しいプルリクエストごとに10個のルールすべてを強制するカスタムチェックを設定することができる。この記事では、各ルールについて、なぜそれが重要なのかを説明し、間違ったアプローチと正しいアプローチの両方を示すコード例を提供する。

なぜこのルールが重要なのか

NASAのルールは、宇宙船の制御やフライト・ソフトウェアなどのミッション・クリティカルなアプリケーションに不可欠な可読性、解析性、信頼性に重点を置いている。不明瞭なCの構造を認めず、防御的なチェックを強制することで、ガイドラインはコードのレビューを容易にし、その正しさを証明する。このガイドラインは、静的解析ツールが見落としがちなパターンに対処することで、MISRA Cのような標準を補完している。例えば、再帰や動的メモリを避けることで、リソースの使用状況を予測しやすくし、戻り値のチェックを強制することで、コンパイル時に多くのバグを発見することができる。

実際、トヨタの電子スロットル・ファームウェアのような大衆市場向け組込みシステムに関する NASA の調査では、何百ものルール違反が見つかった。これは、実世界のプロジェクトが、これらのルールが防ぐように設計されているのと同じ問 題にしばしば遭遇することを示している。リストの各ルールは、一般的なエラーのクラス(制御されないループ、ヌル・ポインタの非参 照、見えない副作用など)を防止する。 これらを無視すると、微妙な実行時障害やセキュリティ・ホール、非決定論的な動作につながる可能性がある。対照的に、10個のルールすべてに従うことで、静的検証はより扱いやすくなります。

自動化ツールは重要だ。コード品質プラットフォームは、禁止された構成やパターンを検出するように設定できる。これらのルールは、すべてのプルリクエストに対して自動的に実行され、コードがマージされる前に問題を検出する。

コンテクストとルールの架け橋

個々のルールに飛び込む前に、その背景を理解することが重要だ:

  • ターゲット言語:NASAの "Power of 10 "ルールはC言語用に書かれたもので、ツールサポート(コンパイラ、アナライザ、デバッガ)が充実している反面、未定義の動作で悪名高い言語でもある。ガベージコレクションや高度なメモリ管理は想定されていない。シンプルで構造化されたC言語のみを使用することで、静的解析を活用してプログラムの特性を証明することができる。
  • 静的分析:自動チェックを容易にするために、多くのルールが存在する。例えば、再帰を禁止し(ルール1)、ループの境界を要求する(ルール2)ことで、関数の反復回数やスタック使用量をツールが証明できるようになる。同様に、複雑なマクロの禁止やポインタの制限(ルール8-9)により、コードのパターンをプリプロセッサのマジックや複数の間接関数に隠さずに明示することができる。
  • 開発ワークフロー:最新のDevSecOpsパイプラインでは、これらのルールはCIチェックの一部になる。コード品質ツールはGitHub、GitLab、またはBitbucketと統合して、各プルリクエストをレビューし、単純な問題とより複雑なパターンの両方を検出することができます。例えば、"gotoや再帰的な関数呼び出しの使用にフラグを立てる"、"各ループにリテラル制限があることを確認する "など、NASAのガイドラインごとにカスタムルールを作成することができます。一度設定すると、これらのルールは今後のコードスキャンで自動的に適用され、違反を早期に発見し、修正方法のガイダンスを提供します。

要するに、NASAの10のルールは、防御的で分析可能なCプログラミングを体現している。以下に各ルールを列挙し、良いコードと悪いコードがどのようなものかを示し、なぜそのルールが存在するのか、どのようなリスクを軽減するのかを説明する

NASAの10のコーディング・ルール

1.複雑な制御フローを避ける

goto、setjmp、longjmpを使用せず、コードのどの部分でも再帰関数を書かないこと。

非準拠の例

// Non-compliant: recursive function call
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n-1);   // recursion (direct)
}

準拠例(ループを使用)

// Compliant: uses an explicit loop instead of recursion
int factorial(int n) {
    int result = 1;
    for (int i = n; i > 1; --i) {
        result *= i;
    }
    return result;
}

なぜこれが重要なのか:再帰とgotoは、理屈で説明するのが難しい非線形の制御フローを生み出す。 再帰呼び出しは呼び出しグラフを循環させ、スタックの深さを無制限にする。単純なループと直線的なコードを使うことで、静的アナライザはスタック使用量とプログラムパスを簡単に検証できる。このルールに違反すると、予期しないスタック・オーバーフローや、手作業でレビューするのが難しいロジック・パスにつながる可能性がある。

2.ループは一定の上限を持たなければならない。

すべてのループは、コンパイル時に検証可能な制限を持つべきである。

非準拠の例(非拘束ループ):

// Non-compliant: loop with dynamic or unknown bound
int i = 0;
while (array[i] != 0) {
    doSomething(array[i]);
    i++;
}

準拠例(固定バウンドループ):

// Compliant: loop with explicit fixed upper bound and assert
#define MAX_LEN 100
for (int i = 0; i < MAX_LEN; i++) {
    if (array[i] == 0) break;
    doSomething(array[i]);
}

なぜこれが重要なのか:無制限のループは永遠に実行されたり、リソースの制限を超えたりする可能性がある。 固定境界があれば、ツールは最大反復回数を静的に証明できる。セーフティ・クリティカルなシステムでは、境界がないとループが暴走する可能性がある。明示的な制限値(または静的な配列サイズ)を強制することで、ループが予測可能に終了することを保証する。 このルールがなければ、ループ・ロジックのエラーは展開するまで発見できないかもしれない(例えば、無限ループを引き起こすオフバイワン)。

3.初期化後のダイナミックメモリがない。

実行中のコードではmalloc/freeやヒープの使用を避け、固定またはスタックアロケーションのみを使用する。

非準拠の例(mallocを使用)

// Non-compliant: dynamic allocation inside the code
void storeData(int size) {
    int *buffer = malloc(size * sizeof(int));
    if (buffer == NULL) return;
    // ... use buffer ...
    free(buffer);
}

準拠例(静的割り当て)

// Compliant: fixed-size array on stack or global
#define MAX_SIZE 256
void storeData() {
    int buffer[MAX_SIZE];
    // ... use buffer without dynamic alloc ...
}

なぜこれが重要なのか:特に宇宙船や組み込みコントローラのようにリソースが限られているシステムでは、実行中の動的なメモリ確保は、予測不可能な動作やメモリの断片化、割り当ての失敗につながる可能性があります。mallocやfreeがミッションの途中で失敗すると、ソフトウェアがクラッシュしたり、予測できない動作をしたりする可能性があります。固定サイズまたはスタック割り当てメモリのみを使用することで、決定論的な動作を保証し、検証を簡素化し、実行時のメモリ・リークを防ぎます。

4.機能が1ページに収まる(~60行)。

各関数は短く(およそ60行以下)。

非準拠の

// Non-compliant: hundreds of lines in one function (not shown)
void processAllData() {
    // ... imagine 100+ lines of code doing many tasks ...
}

準拠例(モジュラー機能)

// Compliant: break the task into clear sub-functions
void processAllData() {
    preprocessData();
    analyzeData();
    postprocessData();
}
void preprocessData() { /* ... */ }
void analyzeData()   { /* ... */ }
void postprocessData(){ /* ... */ }

なぜこれが重要なのか:非常に長い関数は、1つのユニットとして理解し、テストし、検証するのが難しい。 各関数を1つの概念的なタスクに限定する(そして印刷されたページ内に収める)ことで、コードレビューと静的チェックが扱いやすくなります。 関数があまりにも多くの行にまたがっていると、論理的なエラーや境界条件を見逃してしまう可能性がある。コードを小さな関数に分割することで、明快さが向上し、他のルール(関数ごとのアサーション密度やリターン・チェックなど)の適用が容易になります。

5.1つの関数に少なくとも2つのassert文を使用する。

各機能は防御チェックを行うべきである。

非準拠の例(安全措置なし):

int get_element(int *array, size_t size, size_t index) {
return array[index];
}

準拠例(アサーション付き):

int get_element(int *array, size_t size, size_t index) {
    assert(array != NULL);        // Assertion 1: pointer validity
    assert(index < size);          // Assertion 2: bounds check
    
    if (array == NULL) return -1;  // Recovery: return error
    if (index >= size) return -1;  // Recovery: return error
    
    return array[index];
}

なぜこれが重要なのか:アサーションは、無効な条件に対する防御の第一線である。 NASAは、アサーションの密度が高ければ高いほど、バグを発見するチャンスが大幅に増えることを発見した。 関数ごとに少なくとも2つのアサーション(前提条件、制限、不変量のチェック)があれば、コードはその仮定を自己文書化し、テスト中に直ちに異常を知らせます。 アサーションがなければ、予期せぬ値が静かに伝播し、エラーの発生源から遠く離れた場所で不具合を引き起こすかもしれない。

6.最小限のスコープでデータを宣言する。

変数はできるだけローカルに保ち、グローバルを避ける。

非準拠の例(gloablデータ):

// Non-compliant: global variable visible everywhere
int statusFlag;
void setStatus(int f) {
    statusFlag = f;
}

準拠例(ローカルスコープ):

// Compliant: local variable inside function
void setStatus(int f) {
    int statusFlag = f;
    // ... use statusFlag only here ...
}

なぜこれが重要なのか:スコープを最小化することで、結合や意図しない相互作用を減らすことができる。 ある変数が関数内でしか必要とされない場合、それをグローバルに宣言すると、他のコードが予期せずその変数を変更してしまう危険性がある。 データをローカルに保つことで、各関数はより自己完結的になり、副作用がなくなる。 (グローバルな状態を再利用するような)違反は、エイリアシングや予期せぬ変更によって見つけにくいバグにつながる可能性があります。

7.すべての関数の戻り値とパラメータをチェックする。

呼び出し側は、空でないすべての戻り値を検査しなければならない。すべての関数は、その入力パラメータを検証しなければならない。

非準拠の例(戻り値を無視)

int bad_mission_control(int velocity, int time) {
    int distance;
    calculate_trajectory(velocity, time, &distance);  // Didn't check!
    return distance;  // Could be garbage if calculation failed
}

準拠例

int good_mission_control(int velocity, int time) {
    int distance;
    int status = calculate_trajectory(velocity, time, &distance);
    
    if (status != 0) {  // Checked the return value
        return -1;  // Propagate error to caller
    }
    
    return distance;  // Safe to use
}

なぜこれが重要なのか:戻り値や無効なパラメータを無視することは、バグの主な原因である。例えば、mallocのチェックを怠ると、ヌル・ポインタの非参照につながるかもしれない。同様に、入力(配列のインデックスやフォーマット文字列など)を検証しないと、バッファ・オーバーフローやクラッシュを引き起こす可能性があります。NASAでは、すべてのreturnを処理し(あるいは明示的にvoidにキャストして意図を伝える)、すべての引数を検証する必要がある。このキャッチオール・アプローチにより、エラーが黙って無視されることはありません。

8.プリプロセッサをインクルードと単純なマクロに限定する。

複雑なマクロや条件付きコンパイルのトリックは避ける。

非準拠の例(コンプレックス・マルコ

#定義DECLARE_FUNC(name) void func_##name(void)

DECLARE_FUNC(init);  // 以下のように拡張: void func_init(void)

準拠例(シンプルなマクロ/インライン):

// Compliant: use inline function or straightforward definitions
static inline int sqr(int x) { return x*x; }
#define MAX_BUFFER 256

なぜこれが重要なのか:複雑なマクロ(特に複数行のマクロや関数のようなマクロ)は、ロジックを隠蔽し、制御フローを混乱させ、静的解析を妨害します。 プリプロセッサを些細なタスク(定数やヘッダーなど)に限定することで、コードを明示的に保つことができます。 例えば、マクロをインライン関数に置き換えることで、型チェックとデバッグ性が向上する。 このルールがなければ、微妙なマクロ展開のバグや条件付きコンパイルのエラーが、気づかずにレビューをすり抜けるかもしれない。

9.ポインタの使用を制限する

int**や関数ポインタは避ける。

非準拠の例(複数のインデレクション):

// 非準拠:ダブル・ポインタと関数ポインタ
int**doublePtr;
int(*funcPtr)(int) = someFunction;

準拠例(シングルポインタ):

// 準拠:単一レベルポインタ、関数ポインタなし
int*singlePtr;
// 関数ポインタの代わりに明示的呼び出しを使用
intresult = someFunction(5);

なぜこれが重要なのか:複数のレベルのポインタや関数ポインタがあると、データの流れが複雑になり、どのメモリやコードがアクセスされているのかを追うのが難しくなる。 スタティック・アナライザーは各インダイレクトを解決しなければならないが、これは一般的に決定不可能な場合がある。シングル・ポインターの参照に制限することで、コードはよりシンプルで安全になる。 これに違反すると、不明確なエイリアシング(あるポインタが別のポインタを介してデータを変更する)や予期しないコールバック動作につながる可能性があり、どちらもセーフティ・クリティカルなコンテキストでは危険である。

10.すべての警告を有効にしてコンパイルし、修正する。

すべてのコンパイラ警告を有効にし、リリース前に対処すること。

非準拠の例(警告付きコード)

// Non-compliant: code that generates warnings (uninitialized, suspicious assignment)
int x;
if (x = 5) {  // bug: should be '==' or initialize x
    // ...
}
printf("%d\n", x);  // warning: 'x' is used uninitialized

準拠サンプル (クリーンコンパイル)

// Compliant: initialize variables and use '==' in condition
int x = 0;
if (x == 5) {
    // ...
}
printf("%d\n", x);

なぜこれが重要なのか:コンパイラの警告は、しばしば本物のバグ(初期化されていない変数、型の不一致、意図しない代入など)にフラグを立てる。 NASAのルールでは、いかなる警告も無視しないことを義務付けています。リリースの前に、最大冗長度設定のもとで警告なしでコンパイルすること。このやり方は、多くの些細なミスを早期に発見する。警告が解決できない場合は、コードを再構築するか、そもそも警告が発生しないように文書化すべきである。

これらのルールはそれぞれ、隠れたエラーのカテゴリーを排除する。 これらのルールに従うことで、Cコードの予測可能性と検証可能性が大幅に向上する。

結論

NASAの10個のルール("Power of 10")は、クリティカルなCソフトウェアのための明確で効果的なコーディング基準を提供する。複雑な構造を避け、チェックを強制することで、隠れたバグの可能性を減らし、静的解析を実現可能にしている。最近の開発では、これらのガイドラインはコード品質ツールで自動化できる。NASAのガイドライン違反にフラグを立てるためのカスタム・ルールを定義することができ、これらのルールはすべてのプル・リクエストで実行され、開発者に即座にフィードバックを提供することができる。

これらのチェックを早期に採用することで、より安全で質の高いコードとなり、保守も容易になる。航空宇宙以外の分野でも、この原則は変わりません:小さくて明確な関数、明示的なループ、防御的なプログラミング、身の毛もよだつようなポインタの体操をしないことです。 コード品質ツールでこれらのルールに従い、自動化することで、チームはエラーを早期に発見し、より信頼性の高いソフトウェアを出荷することができる。

よくある質問

ご質問は?

NASAのルールは宇宙や組み込みプロジェクトだけのものですか?

そんなことはない。これらのルールは、セーフティ・クリティカルな文脈で生まれたものだが、一般化することもできる。保守性と信頼性を重視するCプロジェクトであれば、どのようなものでも恩恵を受けることができる。実際、このルールはMISRA Cのような業界標準を補完するものであり、NASA以外の多くの開発者は、このガイドラインのサブセットでも実施することでコード品質が向上することを発見している。

これらのルールを自動的に適用するにはどうすればよいですか?

静的解析ツールやコード・レビュー・ツールを使う。Aikido SecurityのCode Qualityツールでは、カスタム・ルールを作成することができます。各ガイドラインに対して小さなルール(例えば、60行以上のgotoや関数にフラグを立てるルール)を作成し、Aikido保存することができる。Aikido 、新しいプルリクエストをカスタムルールと照らし合わせてチェックし、違反があればマージをブロックする。これはGitHub/GitLab/Bitbucketなどとシームレスに統合される。

なぜ動的メモリと再帰を避けなければならないのか?

動的メモリアロケータ(mallocなど)は失敗したり、予測できない動作をすることがあり、管理されていない再帰はスタックの使用を無制限にする。クリティカルなソフトウェアでは、リソースの境界を証明し、最悪のケースを処理しなければならないことが多い。実行時にmallocと再帰を禁止することで、すべてのメモリと呼び出しの深さを事前に知ることができる。これにより、メモリ・リークやオーバーフロー、スタック・オーバーフローのような古典的なバグを防ぐことができる。

もし、私のプロジェクトがこれらのルールのどれかを破る必要があるとしたら?

NASAのガイドラインは設計上厳しい。どうしても逸脱しなければならない場合(例えば、小さなダイナミック・バッファを使うなど)は、意識的にそうすべきである:例外を文書化し、それを正当化し、場合によっては実行時チェックを追加する。いくつかのルールをエラーではなく警告として扱うことを選択するチームもあるが、最も安全なアプローチは、遵守するようにコードをリファクタリングすることである。NASAのルールは保守的だが、だからこそ機能するのだ。Aikido 他のツールを使えば、優先順位の低いルールとしてマークすることもできるが、それでも根本的な問題に対処するのがベストだ。

Aikido NASAのルール違反を他の問題と区別できるか?

そうだ。Aikidoルールはカスタマイズ可能で、タグ付けもできる。カスタムルールに「NASAルール1」、「NASAルール2」などのラベルを付けることができ、違反がどのガイドラインに違反したかを明確に示すことができます。Aikido また、経時的な分析を追跡するので、コードベース全体の「NASAルール遵守率」のようなメトリクスを見ることができる。このトレーサビリティは、チームが修正に優先順位をつけ、監査時にコンプライアンスを証明するのに役立ちます。

まずは無料で体験

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要。