プログラミングのなんとか安全まとめ
プログラミングのなんとか安全まとめ
安全とは
安全の正確な定義は難しいですが、ここでは「特定のリスクが十分に低減されている状態」について考えたいと思います。
人間社会を例に考えてみると、我々の生活は常に犯罪や災害、疫病などの危険にさらされています。しかし通常の生活において、これらのリスクを強く意識することはありません。それは行政(警察や消防や法制度など)の社会システムが、リスクを十分に低減する処置をとっているからです。
ソフトウエアにおける安全も基本的に同質であり、言語やフレームワーク、開発プロセスによって、リスクを管理することで安全の実現を目指します。
リスク管理は何らかの制限を伴いますので、何を自由とし、リスクとするかは、エコシステム毎に異なります。例えば銃器は、国によって所有に対する規制の強弱が分かれています。しかし認可されている国の治安が、必ずしも悪いわけではありません。社会がそのことを前提に最適化され、住人たちも常に危険の可能性を考慮して行動しているからです。外から見れば気疲れしそうに思えますが、住人たちはそれを当然のこととして慣れているはずです。
また必要があれば、隔離エリアを作って追加のセキュリティチェックを行うことで、安全な環境を作ることができます。ただし、危険性が元々制限されている環境に比べて、遥かに持ち込みが容易ですので、かなり厳重な検査が必要となります。
プログラミング言語も同様に生のメモリアクセスやヌルの許容について違いがあります。しかしそのことは直ちに不具合に直結しません。熟練者は自然とリスクに注意することができるためです。ただしその注意が見えないコストとして発生することや、熟練者のみでチームを構成し続けられない点は、無視することができません。
また、セキュアコーディングの策定と遵守、という追加の手だてを取ることで、安全を保証することも可能です。しかし人間による逸脱チェックには限界があり、ゼロリスクの保証まではできません。例えば Chromium のような優れたプロジェクトであっても、不具合の七割は不適切なメモリだそうです。このことから、言語やフレームワーク選定の際には、その保証する安全性についても加味する必要があります。
プログラミング言語の安全
以下で解説するコーディングにおける安全とは、プログラムが未定義な、不正な動作をしない…平たく言えば 暴走しない ことを指します。
アプリケーションが捕捉しない異常が発生したとき、言語やフレームワークは、そのアプリケーションがどう処理を継続すれば正常なのか判断できません。そのような場合、唯一確かな前提知識 止まっているプログラムはそれ以上の害をなさない に基づき、縮退処理を行います。止めずに処理を継続するには、アプリケーションが自前の異常系を持つ必要があります。
安全、という言葉の響きから「ちゃんと動く」ことを期待したくなりますが、実際にはこのように「ちゃんと止まる」や「中途半端に動き出さない」ことで実現される安全が多くあります。
C言語の処理に欠陥があった場合の代表的な現象が、セグメンテーション違反によるプロセスの強制終了ですが、これは上記の安全とは大きくかけ離れています。これはプログラムが不正な動作を続けた結果、たまたま不正なメモリ領域にアクセスし、ハードウェアとOSの提供するメモリ保護機構に引っかかっているだけです。
具体的には、以下の影響があります。
- 違反の瞬間すべての処理を強制的に打ち切るため、共有メモリやファイルなどの資源開放が行われず、リークします。
- 不正な動作の影響を予想できません。たまたま開発環境では処理を継続してしまい、正常動作に見えていたプログラムが、本番環境でデータ破壊などの深刻な不具合を生むことがあります。
- 実行環境依存となる未定義動作を意図的に悪用される危険があります。例えばメモリ破壊によって特定のコードにジャンプするよう書き換える、などです。任意のコードが実行される脆弱性、というものです。
一方、安全な言語であれば、不正を見逃しなくキャッチし、すべての例外処理を実行した上で、デバッグ用にスタックトレースを出力します。似たようなエラー出力に見えても、大きく違った結果が生まれているのです。
安全の例
"安全" の名がつく代表例を紹介します。
型安全
型システムが提供する以外の手段でデータを変更できないことです。逸脱の例として、継承関係を無視したキャストなどが挙げられます。C, Java, Go などの静的型付け言語では、コンパイル時の型検査によって多くのエラーを検出できます。ただし型検査にはヌルポインターアクセスなど、実行時に行われるものもあります。
JavaScriptやPythonなどの動的型付け言語はメソッドの呼び出し前に検査を行います。そのため基本的に型安全と呼べますが、実行時検査については当たり前過ぎて特に話題にはなりません。これらの言語で型安全に触れるときは、TypeScriptや型ヒントなどの静的検査が主眼のようです。
メモリ安全
領域外アクセス、ヌルポインターアクセス、未初期化ポインタ、二重開放などによる、未定義のメモリアクセスを発生させないことです。
これは、実行中の検査で IndexOutOfBoundsException や NullPointerException 例外をスローする、初期値未指定時はゼロクリアする、などの動作が規定されていることを意味します。メモリ安全な言語でも上記の不具合を発生させてしまう可能性はありますが、C言語のように実行毎に挙動が違う、という事態に陥ることはありません。
メモリ安全が破られると型の安全も維持できません。そのため両者は関係がありますが、IndexOutOfBoundsException などは言語の型システムと言うよりコレクションクラスが機能として提供するものです。
生のメモリ操作はドライバ開発などのシステムプログラミングではどうしても必要な場面があります。しかし言語がメモリ操作を自由に許容する場合、動的なメモリ使用に対して、コンパイラや静的解析で実行前にすべてのメモリ不具合を検出するのは不可能です。
一方でユニットテストなどの動的検査についても、特殊な組み合わせ条件下でのみ不具合発生したり、不正アクセスが発生しても異常終了に陥らず、テスト疎通してしまうこともあり、安心しきれるものではありません。 C/C++ では Valgrind や sanitizers などのデバッグツールによって検出率を高められますが、速度が低下するため実環境では実行できない場合もあります。そのため言語レベルでの保証がない場合、リスクゼロの証明は非常に困難です。
スレッド安全
異なるスレッドから同時に呼び出されても、内部状態の整合が崩れないことです。
スレッドによる並列処理はメモリ空間を共有しますので、プロセスによる実装に比べ高速化しやすいですが、常に競合に気を使う必要があります。スレッド安全を実現するにはまず、メンバ変数などの内部状態を極力持たないようにすることが重要です。持たざるを得ない場合には、排他制御などの処置が必要となります。
アプリケーションのスレッド安全は、スレッド安全と表記されたクラスを組み合わせるだけでは実現できません。単独のクラスがスレッド安全でも、それを複数個メンバとして持ち、それらの状態組み合わせに条件がある場合、不整合を起こさない検討が必要です。
例えばロギングクラスが「ファイルパス文字列」と「ファイルオープン済みフラグ」を保つ場合、文字列操作やブール型がスレッド安全だったとしても、十分に配慮しないと「ファイルオープン済みだがファイルパスが空」になる瞬間を作ってしまう可能性があります。
例外安全
例外がスローされたときの整合を維持することです。詳細は Microsoft の “例外の安全性を設計する” を参照ください。
- 基本保証
- オブジェクトの内部状態が整合している。メモリバッファ、ファイル、コネクション、描画ハンドルなどのリソースがリークしてない。整合しさえすればよいので、無効状態や初期状態に遷移してもよい。
- 強い保証
- 処理前の状態に復元する。処理前の状態を保持しておき、処理成功時のみ実行結果とスワップすることにより実現する。
- 投げない保証
- 名前の通り。正しいRAIIを維持するにはデストラクタなどで投げないことが前提となる。
Null安全
Kotlinで普及しC#などに導入された概念です。
Javaなどの参照型はヌル値を許容しているため、オブジェクトへの参照は正確には「そのオブジェクトもしくはヌル」を指しています。このヌルへのアクセスは実行時のNullPointerException としてしか検出できず、多くのプログラマーを悩ませてきました。
そこでヌルを取りうる型をNullableとして定義し、それ以外の型でのヌル代入を禁止することで、意図しないヌルへのアクセスをコンパイル時などの実行前チェックの段階で検出できるようにしたものです。
なおC++の参照型はポインタ型と異なりNON-NULLである点が大きな特徴です。
機能安全
”ソフトウェア 安全”で検索すると高い確率で本件に遭遇しますが、上記で説明した安全とは大きく異なる概念です。ここで対象とする安全は、世間一般で言う安全…人や物への危害を加えないことを指します。
最終的には意図しない動作を防ぐ実装が求められるため、上記の技法と重複する面もありますが、対象とする範囲も少し異なります。
発生確率の低減を目指すため、悪意を持った脆弱性への意図的な攻撃は対象外ですし、情報漏洩なども人や物への損傷がないため、対象外です。これらの分野はセキュリティとして別途検討する必要があります。(製品紹介などで「セキュリティ&セーフティを実現する弊社ソリューション」などのタイトルで紹介されるものですね)
機能安全の説明でよく出てくる例が鉄道の交通事故です。線路が高架橋になっている場合、交通事故のリスクはほぼ無視できます。これを 本質安全 と呼びます。一方、線路は平地に敷いたまま、踏切を追加することで安全を確保する場合もあります。このなんらかの機能によって実現される安全が 機能安全 です。
機能安全規格
機能安全には高い信頼性が求められます。高い稼働率を達成し、万一の故障にもフェールセーフになる必要があります。この信頼性を実現するための開発プロセスをまとめたものが、IEC61058やISO26262などの 機能安全規格 です。システム開発における機能安全対応とは、これらの国際規格が定める開発プロセスに基づいて開発すること、及びその証憑を整備することを示します。
ハードウェアの故障は、部品の劣化などによって偶発的に発生します(ランダム故障)。そこでハードウェア開発においては、各構成部品の故障率の積み上げによって安全性を担保します。しかしソフトウェア開発においては、不具合は存在する・しないの2択です(決定論的故障)。機能安全規格では、厳格な開発プロセスの運用運用でソフトウェア故障のリスクを低減できる、という立場を取ります。(少なくとも、安全のために考えうる検討が十分にされたことの証明にはなります。)
まとめ
プログラミングにまつわる様々な安全について俯瞰しました。安全の指し示す対象はコンテキストにより様々であり、きちんと理解するには、前提を理解する必要があります。
Discussion
スレッド安全
次と同じ