web-dev-qa-db-ja.com

プログラムで任意のコードの安全性を評価することは可能ですか?

最近、安全なコードについて多くのことを考えています。スレッドセーフ。メモリセーフ。 segfaultセーフで自分の顔を爆発させない。ただし、質問を明確にするために、Rustの安全モデルを定義として使用します。

多くの場合、Rustがunsafeを必要とすることで証明されるように、並行性などの非常に合理的なプログラミングのアイデアがあるため、安全性を確保することは少し大きな問題ですcannotをRustに実装する場合、unsafeキーワードを使用しないでください。ロックを使用すると、同時実行を完全に安全にすることができます。 、ミューテックス、チャネル、メモリ分離、または何があるか、これにはunsafeを使用してRustの安全モデルので作業し、手動でコンパイラに保証する必要があります "ええ、私は自分のやっていることを知っています。これは安全ではないように見えますが、数学的には完全に安全であることを証明しました。"

しかし通常、これは手動でこれらのもののモデルを作成し、それらが 定理証明者 で安全であることを証明することになります。コンピュータサイエンスの観点(可能か)と実用性の観点(宇宙の生命を奪うことになるか)の両方から、任意の言語で任意のコードを受け取り、それがそうであるかどうかを評価するプログラムを想像するのは合理的です錆びない」

注意事項

  • これの簡単な解決策は、プログラムが停止しない可能性があることを指摘することです。そのため、 停止の問題 は失敗します。読者に提供されたプログラムが停止することが保証されているとしましょう
  • 「任意の言語の任意のコード」が目標ですが、これはプログラムが選択された言語に慣れていることに依存していることはもちろん承知しています。
9

ここで最終的に話しているのは、コンパイル時間とランタイムです。

コンパイル時間エラーは、考えれば、最終的には、コンパイラーが実行される前にプログラムにどのような問題があるかを判別できることになります。それは明らかに「任意の言語」コンパイラではありませんが、すぐに戻ってきます。コンパイラは、そのすべての無限の知恵では、コンパイラによって決定されるevery問題をリストしません。これは、コンパイラがどの程度適切に記述されているかによって部分的に異なりますが、その主な理由は、runtimeで多くのことが決定されるためです。

ランタイムエラーは、あなたもよく知っているように、プログラム自体の実行中に発生するあらゆる種類のエラーです。これには、ゼロ除算、ヌルポインター例外、ハードウェアの問題、およびその他の多くの要因が含まれます。

ランタイムエラーの性質は、コンパイル時に上記のエラーを予測できないことを意味します。可能であれば、コンパイル時にほぼ確実にチェックされます。コンパイル時に数値がゼロであることを保証できる場合は、特定の論理的な結論を実行できます。たとえば、数値をその数値で除算すると、ゼロで除算することにより算術エラーが発生します。

そのため、非常に現実的な方法で、プログラムの適切な機能をプログラムで保証するという敵は、コンパイル時のチェックではなく実行時のチェックを実行しています。この例として、別のタイプへの動的キャストを実行する場合があります。これが許可されている場合、プログラマーであるあなたは、それが安全なことであるかどうかを知るコンパイラーの機能を本質的にオーバーライドしています。一部のプログラミング言語はこれが受け入れ可能であると判断していますが、他のプログラミング言語は少なくともコンパイル時に警告します。

Nullを許可するとnullポインター例外が発生する可能性があるため、別の良い例はnullを言語の一部にすることです。一部の言語では、明示的に宣言されていない変数がすぐに値を割り当てられることなく宣言されるnull値を保持できるようにすることで、この問題を完全に排除しています(Kotlinなど)。ヌルポインター例外のランタイムエラーを排除することはできませんが、言語の動的な性質を削除することで、このエラーの発生を防ぐことができます。 Kotlinでは、もちろんforce null値を保持する可能性がありますが、これは比喩的な「バイヤーに注意」であることは言うまでもありません。

概念的には、すべての言語のエラーをチェックできるコンパイラを用意できますか?はい、しかし、それはおそらく事前にコンパイルされる言語を必ず提供しなければならない、不格好で非常に不安定なコンパイラでしょう。また、特定の言語のコンパイラがあなたが言及した停止問題など、特定のことを知っている以上、プログラムについて多くのことを知ることはできませんでした。結局のところ、プログラムについて学ぶのに興味深いかもしれないかなり多くの情報を収集することは不可能です。これは証明されているため、すぐに変更される可能性はほとんどありません。

主なポイントに戻ります。メソッドは自動的にスレッドセーフではありません。これには実用的な理由があります。これは、スレッドが使用されていない場合でも、スレッドセーフメソッドの速度が低下するためです。 Rustは、メソッドをデフォルトでスレッドセーフにすることで実行時の問題を排除できると判断しましたが、それが選択です。ただし、コストがかかります。

プログラムの正確さを数学的に証明することは可能かもしれませんが、言語のランタイム機能が文字通りゼロであることは注意が必要です。あなたはこの言語を読んで、何の驚きもなくその言語が何をするかを知ることができるでしょう。言語はおそらく非常に数学的な性質に見えるでしょう、そしてそれはおそらくそこに偶然ではないでしょう。 2番目の注意点は、ランタイムエラーstillが発生することです。これは、プログラム自体とは関係がない可能性があります。したがって、プログラムが実行されているコンピューターに関する一連の仮定が正確であり、変更されないことを前提として、プログラムは正しいことが証明されます。もちろん、常にdoesが発生します。

8
Neil

型システムは、正確性のいくつかの側面を自動的に検証できる証拠です。たとえば、Rustの型システムは、参照が参照オブジェクトよりも長く存続しないこと、または参照オブジェクトが別のスレッドによって変更されていないことを証明できます。

しかし、型システムはかなり制限されています:

  • 彼らはすぐに決定可能性の問題にぶつかります。特に、型システム自体は決定可能である必要がありますが、実際の型システムの多くは誤ってTuring Completeです(テンプレートによるC++とRustトレイトのため))。また、プログラムの特定のプロパティ一般的に、プログラムが停止(または分岐)するかどうかを確認することはできません。

  • さらに、型システムは、理想的には線形時間ですばやく実行する必要があります。可能なすべての証明が型システムで取り上げられるべきではありません。例えば。プログラム全体の分析は通常避けられ、証明は単一のモジュールまたは関数に限定されます。

これらの制限があるため、型システムは検証が容易なかなり弱いプロパティのみを検証する傾向があります。関数が正しい型の値で呼び出されること。それでも表現力が大幅に制限されるため、回避策(Goではinterface{}、C#ではdynamic、JavaではObject、Cではvoid*など)を使用するか、完全に回避する言語を使用することも一般的です静的型付け。

検証するプロパティが強いほど、言語の表現力は低下します。 Rustを作成した場合、正しいことが証明できなかったため、コンパイラが一見正しいコードを拒否する「コンパイラとの戦い」の瞬間を知っています。場合によっては、特定のプログラムをRustで表現することは不可能であることは、その正確性を証明できると確信している場合でも可能です。unsafeメカニズムRustまたはC#を使用すると、型システムの制限を回避できます。場合によっては、チェックをランタイムに延期することも別のオプションになる可能性があります。ただし、無効なプログラムを拒否できない場合があります。 。これは定義の問題です。Rustプログラムは、型システムに関する限りパニックは安全ですが、必ずしもプログラマーまたはユーザーの観点からではありません。

言語は型システムと一緒に設計されています。新しい型システムが既存の言語に課されることはまれです(ただし、MyPy、Flow、TypeScriptなどを参照してください)。言語は、型システムに準拠するコードを簡単に記述できるようにします。型注釈を提供することによって、または制御フロー構造を証明するのが簡単なものを導入することによって。言語が異なれば、ソリューションも異なる場合があります。例えば。 Javaには、Rustの非final変数と同様に、1回だけ割り当てられるmut変数の概念があります。

final int x;
if (...) { ... }
else     { ... }
doSomethingWith(x);

Javaには、すべてのパスが変数を割り当てるか、変数にアクセスする前に関数を終了するかを決定する型システムルールがあります。対照的に、Rustは、宣言されているが設計されていない変数がないことでこの証明を簡略化しますが、制御フローステートメントから値を返すことができます。

let x = if ... { ... } else { ... };
do_something_with(x)

これは、割り当てを理解する上で非常にマイナーなポイントのように見えますが、明確なスコープは、生涯に関連する証明にとって非常に重要です。

Rustスタイルの型システムをJavaに適用する場合、それよりもはるかに大きな問題が発生します。Javaオブジェクトにはライフタイムの注釈が付けられないため、&'static SomeClassまたはArc<dyn SomeClass>。これは結果として生じる証明を弱めます。Javaには不変性のタイプレベルの概念もないため、&&mutタイプを区別できません。オブジェクトをセルまたはセルとして扱う必要があります。ミューテックス、ただしこれはJavaが実際に提供するものよりも強力な保証を想定している場合があります(Javaを変更すると、同期および揮発性でない限り、フィールドはスレッドセーフではありません)。最後に、Rustには、Javaスタイルの実装継承の概念はありません。

TL; DR:型システムは定理を証明します。ただし、決定可能性の問題とパフォーマンスの問題によって制限されます。ターゲットの言語構文が必要な情報を提供しない場合や、セマンティクスに互換性がない場合があるため、1つの型システムを単純に別の言語に適用することはできません。

3
amon

どのくらい安全ですか?

はい、そのような検証プログラムを作成することはほぼ可能です。プログラムは定数UNSAFEを返すだけです。 99%の確率で正しい

安全なRustプログラムを実行しても、誰かがその実行中にプラグを抜くことができるため、理論的には想定されていなくてもプログラムが停止する可能性があるためです。

また、サーバーがバンカーのファラデーケージで実行されている場合でも、ネイバープロセスがロウハンマーエクスプロイトを実行し、安全と思われるRustプログラムの1つでビットフリップを実行する可能性があります。

私が言おうとしていることは、ソフトウェアは非決定的環境で実行され、多くの外部要因が実行に影響を与える可能性があるということです。

冗談はさておき、自動検証

危険なプログラミング構造(初期化されていない変数、バッファオーバーフローなど)を見つけることができる静的コードアナライザーはすでにあります。これらは、プログラムのグラフを作成し、制約(タイプ、値の範囲、順序付け)の伝播を分析することによって機能します。

ところで、この種の分析は、最適化のために一部のコンパイラでも実行されます。

確かに、さらに一歩進んで、同時実行性を分析し、複数のスレッド、同期、および競合状態にまたがる制約の伝播について推論することは可能です。ただし、実行パス間の組み合わせの爆発、および既知の制約をむき出しにする多くの未知の要素(I/O、OSスケジューリング、ユーザー入力、外部プログラムの動作、割り込みなど)の問題にすぐに遭遇します。最小化し、任意のコードに対して有用な自動化された結論を出すことを非常に困難にします。

3
Christophe

チューリングは1936年に停止問題に関する彼の論文でこれに対処しました。結果の1つは、100%の時間でコードを分析し、それが停止するかどうかを正しく判断できるアルゴリズムを作成することが不可能であるということだけです。100%の時間で正しく実行できるアルゴリズムを作成することは不可能です。 「安全性」を含め、コードに特定のプロパティがあるかどうかを判断しますが、定義する必要があります。

ただし、Turingの結果は、(1)コードが安全であると絶対的に判断する、(2)コードが安全でないと絶対的に判断する、または(3)擬人化して手を投げて言う、100%の確率でプログラムが実行できる可能性を排除するものではありません。 「一体、私は知りません。」 Rustのコンパイラは、一般的に言えば、このカテゴリに含まれます。

1
NovaDenizen

プログラムが完全なもの(停止が保証されているプログラムの技術名)の場合、十分なリソースがあれば、プログラム上の任意のプロパティを証明することが理論的に可能です。プログラムが入る可能性のあるすべての潜在的な状態を調査し、それらのいずれかがプロパティに違反していないかどうかを確認できます。 TLA + モデルチェック言語は、このアプローチのバリアントを使用し、すべての状態を計算するのではなく、セット理論を使用して、潜在的なプログラム状態のセットに対してプロパティをチェックします。

技術的には、実際の物理ハードウェアで実行されるプログラムは合計であるか、使用可能なストレージの容量が限られているために証明可能なループであるため、コンピューターが使用できる状態の数は限られています。コンピューターは実際には有限状態機械であり、チューリング完全ではありませんが、状態空間は非常に大きいため、完全に回転しているように見せかけることができます)。

このアプローチの問題は、プログラムのストレージの量とサイズが指数関数的に複雑になり、アルゴリズムのコア以外には何も実行できなくなり、重要なコードベース全体に適用することが不可能になることです。

したがって、研究の大部分は証明に焦点を当てています。カリーとハワードの通信では、正当性の証明と型システムはまったく同じものであると述べられているため、実際の研究のほとんどは型システムの名前で行われています。この議論に特に関連するのは、既に述べたRustに加えて、 Coq および Idriss です。Coqは、反対方向。Coq言語での任意のコードの正当性の証明を取得すると、証明されたプログラムを実行するコードを生成できます。一方、イドリスは依存型システムを使用して、純粋なHaskellのような言語で任意のコードを証明します。これらの両方の言語の機能実行可能なプルーフを生成するという難しい問題をライターにプッシュし、タイプチェッカーがプルーフのチェックに集中できるようにします。プルーフのチェックははるかに簡単な問題ですが、これにより、言語の扱いがずっと難しくなります。

これらの言語はどちらも、証明を容易にするために特別に設計されており、純粋性を使用して、プログラムのどの部分にどの状態が関連しているかを制御します。多くの主流言語では、状態の一部がプログラムの一部の証明に無関係であることを証明するだけでは、副作用と可変値の性質により、複雑な問題になる可能性があります。

1
user1937198