安全性とUnsafeの問題
オペレーティングシステムはどうしても安全でないコードを使用しなければなりません。 このドキュメントでは、安全でないコードを使用するが、OS全体の安全性を維持する 必要があるTockにおける主たる機構を支える根拠を説明します。
static_init!
static_init!の「型」は基本的には次のとおりです。
#![allow(unused)] fn main() { T => (fn() -> T) -> &'static mut T }
つまり、T型の何かを返す関数が与えられた場合、static_init!はstatic
ライフタイムを持つTへの可変参照を返します。
実質的には、これは可変静的変数の宣言と同じことを意味します。
#![allow(unused)] fn main() { static mut MY_VAR: SomeT = SomeT::const_constructor(); }
そして、それへの参照の作成は次のようになります。
#![allow(unused)] fn main() { let my_ref: &'static mut = &mut MY_VAR; }
しかし、static宣言の左辺値はconstでなければなりません(Rustには
pre-initializationセクションがないため)。そのため、static_init!は
基本的にconstではない初期化子を持つ静的変数を許すものです。
これらのケースではどちらも呼び出し元は関数をunsafeでラップしなければ
ならないことに注意してください。可変変数の参照は(エイリアス規則により)
安全ではないからです。
使用
static_init!はTockではカプセルの初期化に使用され、最終的には互いに参照し
合うことになります。いずれの場合もこれらの参照は不変です。これらを静的に
割り当てることは2つの理由で重要です。第一に、リンク時のメモリ逼迫の問題を
表面化するのに役立つからです(これらがスタックに割り当てられた場合、スタック
サイズが適切でなければ、メモリ不足リンクエラーとして簡単に示さることはない
でしょう)。第二に、相互に依存するカプセルのライフタイムは同じである必要が
あり、'staticはこれを実現する便利な方法だからです。
しかし、_誰が_特定のコールを行えるかを強制するために可変参照で始めることが
有用な場合があります。たとえば、SPIドライバにおけるバッファの設定は実用的な
理由から構築後まで延期されますが、プラットフォームの初期化関数だけが
(main関数が起動する前に)呼び出せるようにしたい場合です。プラットフォーム
設定後のすべての参照は不変であり、config_buffersメソッドは引数に
&mut selfを取るので、これは強制されています(注意: これは厳密には必要ではないように見えるので、これができなくても大したことはないかもしれません)。
安全性
static_init!の使用が安全でないものになるのは、可変参照へのエイリアスの
作成に使用された場合です。これが&'static mutを返すという事実が赤旗です
ので、これが何故OKだと考えるかを説明する必要があります。
他の&mutと同じように、それは再借用されるとすぐに使えなくなります。具体的に
Tockで行っていることは、あるケースにおいてstatic_init!を呼び出した直後に
それを可変的に使用し、次にそれを不変的に再借用してカプセルに渡すことです。
あるカプセルが&mutを受け入れた場合、コンパイラは参照を移動しようとし、
その呼び出しが失敗する(すでに他の場所で不変的に再借用されている場合)か、
それ以上の再借用ができなくなります。これは実際に共有参照として使用されない
場合は問題ないことに注意してください(もっとも、そのような使用例はないと
思いますが)。
ただし、static_init!を呼び出すコードが二度実行されないことが重要です。
これは2つの大きな問題を引き起こします。第一に、技術的には複数の可変参照が
発生する可能性があります。第二に、コンストラクタを二度実行することになり、
同一メモリへの複数の参照に関するその他の安全性や機能的な問題が発生する
可能性があります。これは、静的変数への可変参照を取るコードと変わらないと
思います。繰り返しになりますが、重要なのはどちらの場合もunsafeでラップ
しなければならないことです。
代替案
static_init!から代わりに不変静的参照を返すことは技術的には可能だと
思われます。それにはコードを少し変更する必要があり、初期化を特定のカプセル
メソッドに制限することはできませんが、特に大きな問題ではないかもしれません。
また、Option型の何らかの静的変数をどこでも使えるようにする(これも
合理的かもしれません)。
ケイパビリティ: 特定の機能や操作へのアクセス制限
ある種の操作や関数、特にカーネルクレート内のものは、言語的に見れば「安全では
ない」ことはありませんが、隔離やシステム操作の観点からは安全ではありません。
たとえば、プロセスの再起動は概念的には型やメモリの安全を破るものではありません
が(Tockにおけるある実装では破りますが)、カーネル内の任意のコードが任意の
プロセスを再起動できるとしたらシステム全体の安全性を損ねます。したがって、
Tockはrestart_process()のような関数の提供方法に注意しなければなりません。
特に、Rustによってサンドボックス化されるべき信頼できないコードであるカプセルが
restart_process()関数にアクセスできるようにしてはいけません。
さいわい、Rustはこの制限を行うためのプリミティブを提供しています。unsafe
キーワードの使用です。unsafeとマークされたすべての関数は、他のunsafe
関数かunsafeブロックからしか呼び出すことができません。したがって、クレートで
#![forbid(unsafe_code)]属性を使用することにより、unsafeブロックを
定義する機能を削除することによりそのクレートのすべてのモジュールはunsafeと
マークされたすべての関数を呼び出すことができなくなります。Tockではカプセル
クレートにはこの属性を付けています。そのため、すべてのカプセルはunsafe関数
を使用できません。このアプローチは効果的ですが、すべてのunsafe関数への
アクセスを提供するか、全く提供しないかという、非常に粒度の粗いものです。より
微妙な制御を提供するために、Tockはケイパビリティ(Capabilities)と
呼ばれる機構を持っています。
ケイパビリティとは、本質的には、特定の関数を呼び出すために必要なゼロメモリ
オブジェクトのことです。抽象的には、restart_process()のような制約のある
関数は呼び出し元が特定のケイパビリティを持っている必要があります。
restart_process(process_id: usize, capability: ProcessRestartCapability) {}
ケイパビリティを持たずにその関数を呼び出そうとするとコンパイルできないコードに
なります。ケイパビリティの不正な使用を防ぐために、ケイパビリティは信頼された
コードでしか作成することができません。Tockではunsafeなトレイトとして
ケイパビリティを定義することでこれを実装しています。これはunsafeな呼び出し
ができるコードによるオブジェクトにしか実装することができません。そのため、
信頼できないカプセルクレートのコードはそれ自身ではケイパビリティを生成する
ことができませんので、代わりに別のクレートのモジュールからケイパビリティを
渡さなければなりません。
ケイパビリティは、非常に広範な目的のためにも、非常に狭い目的のためにも定義する こともでき、コードは複数のケイパビリティを「要求」することができます。Tock では、1つのオブジェクトに複数のケイパビリティトレイトを実装することにより 複数のケイパビリティを渡すことができます。
ケイパビリティの例
-
Tockにおいてケイパビリティがいかに有用であるかの一例として、プロセスの ロードがあります。プロセスのロードはボードの責任として残されています。 なぜなら、ボードは何らかの方法でプロセスを処理するか、ユーザランドプロセスを 全くサポートしないかを選択できるからです。しかし、カーネルクレートはプロセスを発見してロードするTockの標準的方法を提供する
load_processes()という 便利な関数を提供しています。この関数はカーネルクレートで定義されているので すべてのTockボードが共有できます。これは関数をpublicにするよう強制します。 これは、カーネルクレートにアクセスできる_すべての_モジュールがload_processes()を呼び出すことができるという効果があります。ただし、 これを二回呼び出すと望まない結果を起こす可能性があります。一つの方法は、 関数をunsafeとマークし、信頼できるコードしかそれを呼び出せないように することです。これは効果的ですが、明示的ではなく、言語レベルの安全性と システムの操作レベルの安全性を混同してしまいます。代わりにload_processes()の呼び出し元が何らかのケイパビリティを持つことを要求することにより、 呼び出し元の期待がより明確になり、unsafeな関数を別の目的で利用する必要が なくなります。 -
同様の例として、
restart_all_processes()のような関数があります。 この関数は、ボード上のすべてのプロセスをフォルト状態にし、すべてのグラントを 削除した上で元の_start地点から再起動します。繰り返しになりますが、これは システムレベルの目標に違反する可能性がある関数ですが、特定の状況やアプリが 失敗した際にグラントのクリーンアップをデバッグする用途には非常に役に立ちます。 しかし、load_processes()とは異なり、特定のイベントに反応したり、 ウォッチドッグとして動作させるためにカプセルがrestart_all_processes()を呼び出せるようにすることは理にかなっているかもしれません。その場合、unsafeであるとマークしてアクセスを制限してもうまくいきません。カプセルはunsafeなコードを呼び出せないからです。ケイパビリティを使用することで、 正しいケイパビリティを持つ呼び出し元だけがrestart_all_processes()を 呼び出すことができ、個々のボードがどのカプセルにどのケイパビリティを付与する かについて非常に明確にすることができます。