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


9

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

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

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

警告

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

2
任意のコード?いいえ。I / Oとハードウェアの例外のために、最も有用なコードの安全性を証明することさえできないと思います。
Telastyn 2018年

7
なぜ停止問題を無視するのですか?あなたが言及した例のひとつひとつ、そしてそれ以上のものは、停止問題、関数問題、ライスの定理、または他の多くの決定不能定理のいずれかを解決することと同等であることが証明されています:ポインター安全性、メモリー安全性、スレッド-safety、exception-safety、purity、I / O-safety、lock-safety、進捗の保証など。ホールティング問題は、知りたいと思われる最も単純な静的プロパティの 1つであり他のすべてのリストははるかに困難です。
イェルクWミッターク


あなたは絶対にしません使用する必要がunsafe並行コードを書くために錆を。それらは、同期プリミティブから俳優にインスパイアされたチャネルに至るまで、利用可能ないくつかの異なるメカニズムです。
RubberDuck

回答:


8

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

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

実行時エラーは、ご存じのとおり、私自身もそうですが、プログラム自体の実行中に発生するあらゆるタイプのエラーです。これには、ゼロ除算、ヌルポインター例外、ハードウェアの問題、およびその他の多くの要因が含まれます。

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

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

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

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

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

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


3

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

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

  • 彼らはすぐに決定可能性の問題に遭遇します。特に、型システム自体は決定可能である必要がありますが、実際の型システムの多くは誤ってTuring Completeです(テンプレートによるC ++と特性によるRustを含む)。また、彼らが検証しているプログラムの特定のプロパティは、一般的なケースでは決定できない可能性があります。最も有名なのは、一部のプログラムが停止(または発散)するかどうかです。

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

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

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

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

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型を区別できません。オブジェクトはCellまたはMutexとして扱う必要がありますが、これはJavaが実際に提供するよりも強力な保証であると想定される場合があります(Javaフィールドの変更は、同期および揮発性でない限りスレッドセーフではありません)。最後に、RustにはJavaスタイルの実装継承の概念がありません。

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


3

どのくらい安全ですか?

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

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

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

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

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

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

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

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


1

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

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


「わからない」オプションがある限り、そうですか?
TheEnvironmentalist

1
要点は、プログラム分析プログラムを混乱させる可能性のあるプログラムを作成することは常に可能であるということです。完璧は不可能です。実用的かもしれません。
NovaDenizen

1

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

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.