Safe Haskellは、バージョン7.2でGHCに実装された言語拡張である。これは、コードが使うことが許されるGHC Haskellの機能を限定することによって、unsafeなコードを信頼されたコードベースに安全な形で含めることができるようにする。単純化して言うと、これはプログラム内の型を信頼に足るものにする。Safe Haskellは最小限のものであることを目標としていて、その上により高度な安全システムを構築するのに必要十分な強さの保証を提供する。
これがSafe Haskellの動機となる使用例であるものの、Safe Haskellが追跡し強制するものが、通常のHaskellで保証されるよりも厳格な型安全性であるということを理解するのは重要である。これの一環として、Safe HaskellはGHCによるコンパイルのたびに実行され、明示的にSafe Haskellを使っていないモジュールについてさえ安全性を追跡し推論する。これについてのさらなる詳細は7.27.5. Safe Haskellの推論を参照して欲しい。これはまた、セキュリティの観点からは奇妙に見えるが型安全性を追跡するという角度からは論理的な設計判断があることも意味する。現在の設計について、またこのセキュリティと型安全性の間の緊張関係について、フィードバックを歓迎する。
Safe Haskellの設計は以下の面にわたっている。
一方で、Safe Haskellは、コンパイル時安全性を提供しない。コンパイル時には、例えばプリプロセッサを変更するフラグによって任意のプロセスが起動され得る。これを操作することで、コンパイル時にユーザのシステムを危険にさらしたり、コンパイルの直前にソースコードを改変することで設定されたSafe Haskellのフラグに変更を加えることができる。これについては、節7.27.7. 安全なコンパイルにさらなる議論がある。
Safe Haskellは以下の二つの利用例を念頭に置いて設計されている。
IOモナドを通した純粋な関数と作用のある関数の分離を提供している。しかし、この型システムには抜け穴がいくつかある。最も分かり易いのは関数unsafePerformIO :: IO a -> aである。Safe Haskellのsafe language方言はこのような関数の利用を禁止する。これによってHaskellコードを分析したり論証したりし易くなるので、色々の目的のために便利である。また、このようなunsafeな関数はどうしても必要でない限り使うべきでないという既存のHaskellコミュニティの文化を成文化するものでもある。こういう訳で、(-XSafeを使った)safe languageの利用は、-Wallの機能と似て、良いスタイルを強制する手段だととらえることができる。情報フロー制御、capability-basedセキュリティ、暗号化されたデータを扱うためのDSL、といったシステムは、単に一つのライブラリとしてHaskell言語に組み込むことができる。しかしこれらのシステムは、unsafePerformIOのような関数が許される一般の場合には成り立たないようなHaskell言語の性質が保証されることを要求する。Safe Haskellは、コンパイルされたコードの性質に関して、このようなシステムを構築するのに十分なだけの保証を与えるように設計されている。
例として、プラグイン作者が信用されず、悪意のある第三者かもしれないようなプラグインシステムのためのインタフェースを定義してみよう。このために、プラグインのインタフェースを純粋な関数にするか、IO動作の安全なサブセットしか実行することを許されないような制限されたIOモナドにする。ここではプラグインのインタフェースを以下のようにする。すなわち、プラグインモジュールDangerは、型RIO ()を持つ単一の計算Danger.runMeを定義することを要求される。ただしRIOは以下のように定義された新しいモナドである。
-- 以下のSafe Haskellのプラグマはどちらでも良い
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE Safe #-}
module RIO (RIO(), runRIO, rioReadFile, rioWriteFile) where
-- UnsafeRIOシンボルがこのモジュールからエクスポートされていないのに注意!
newtype RIO a = UnsafeRIO { runRIO :: IO a }
instance Monad RIO where
return = UnsafeRIO . return
(UnsafeRIO m) >>= k = UnsafeRIO $ m >>= runRIO . k
-- ファイル名にアクセスが許可されている場合、かつその場合に限りTrue
pathOK :: FilePath -> IO Bool
pathOK file = {- ファイル名についての何らかのポリシーの実装 -}
rioReadFile :: FilePath -> RIO String
rioReadFile file = UnsafeRIO $ do
ok <- pathOK file
if ok then readFile file else return ""
rioWriteFile :: FilePath -> String -> RIO ()
rioWriteFile file contents = UnsafeRIO $ do
ok <- pathOK file
if ok then writeFile file contents else return () Dangerを、新しいSafe Haskellのフラグである-XSafe付きでコンパイルする {-# LANGUAGE Safe #-}
module Danger ( runMe ) where
runMe :: RIO ()
runMe = ...Safe Haskellの詳細に入る前に、この設計がSafe Haskellなしではうまく行かない理由の一部を指摘しておこう。
IOのラッパであるRIOによって制限している。Dangerの作者は、単にunsafePerformIO :: IO a -> aを使って任意のIOアクションを純粋関数として実行することでこの制限を覆すことができる。 UnsafeRIO構築子にアクセスできないことを前提としている。残念なことにTemplate Haskellを使うとモジュール境界を破壊することができ、この構築子へのアクセスを得ることもできるだろう。 これらの攻撃を止めるためにSafe Haskellを使うことができる。これには、RIOモジュールを-XTrustworthyフラグ付きでコンパイルし、Dangerモジュールを-XSafeフラグ付きでコンパイルする。
-XSafeフラグを使うことで、利用可能なHaskellの機能を安全なサブセットに限定してDangerモジュールをコンパイルすることができる。これには、unsafePerfromIO、Template Haskell、純粋なFFI関数、一般化newtype deriving、RULESを禁止し、重複インスタンスの操作に制限を加えることを含む。さらに、-XSafeは、Dangerがインポートできるモジュールを信頼されているとみなされるもののみに制限する。信頼されるモジュールは、-XSafe付きでコンパイルされたもの(この場合、コードが安全であることをGHCが機械的に保証する)または、-XTrustworthy付きでコンパイルされたもの(この場合、モジュール作者がこのモジュールが安全だと主張する)である。
これが、RIOモジュールがDangerモジュールからインポートできるように-XTrustworthy付きでコンパイルされている理由である。-XTrustworthyは、-XSafeと異なり、モジュールについていかなる制限も課さない。代わりに、コードが内部的にunsafeな機能を使っていても、安全に使えるAPIしか露出していないということをモジュール作者が主張する。-XTrustworthy自体が、そのモジュールを信頼されたものにするのである。ここで、問題が一つある。あらゆるモジュールのあらゆる作者が-XTrustworthyを使えるのである。trustworthyモジュールの利用を制御するため、-fpackage-trustフラグを使うことが推奨される。このフラグは、trustworthyモジュールの信頼検査へ条件を追加する。その場合、Trustworthyとされたモジュールが信頼され、-XSafeでコンパイルされるコードから使うことを許されるためには、コードをコンパイルしているクライアントCが、このTrustworthyとされたモジュールが所属するパッケージを信頼する旨をGHCに伝えなければならない。これは実質的に、「このパッケージは-XSafeでコンパイルされる信頼されないモジュールによって使えるTrustworthyモジュールを含んでいるが、私はこのパッケージの作者を信頼し、このモジュールが安全なAPIしか露出しないことを信頼する」と言う手段をCに与える。パッケージへの信頼はいつでも変えられるので、もしパッケージに脆弱性が見付かったら、Cはそのパッケージが信頼されないと宣言し、そのパッケージに対する将来のいかなるコンパイルも失敗するようにできる。この機構についてのより詳細な概観は7.27.4. 信頼とSafe Haskellのモードを見よ。
例では、RIOはTrustworthyと標示されているのでDangerはRIOをインポートできる。よって、DangerはrioReadFileとrioWriteFileの各関数を使って許可されたファイル名にアクセスできる。次に主アプリケーションがRIOとDangerの両方をインポートし、RIO.runRIO Danger.runMeをIOモナド中で呼ぶことでプラグインを走らせる。pathOKの検査によって認められたパスを持つファイルにしかIOできないと分かっているので、このアプリケーションは安全である。
IOモナドの関数は通常通りに振る舞うことが許される。しかし、純粋な関数は全て、その型に従って実際に純粋であることが保証される。この性質によって、safe languageの利用者は型を信頼することが可能になる。これは、例えばunsafePerformIO :: IO a -> aがsafe languageにおいて禁止されることを意味している。 これらの三つの性質は、safe languageにおいて、型を信頼できること、コードがモジュールのエクスポートリストに従うこと、コンパイルに成功したコードが通常持つべき意味と同じ意味を持つこと、を保証する。
ではsafe languageの詳細を見ていこう。safe lauguage方言(-XSafeによって有効になる)においては、以下の機能は完全に無効になる。 -XSafeでコンパイルされたモジュールMで定義されたRULESは全て捨てられる。MがインポートするTrustworthyモジュールで定義されたRULESはなお有効であり、通常通り発動する。-XSafeでコンンパイルされたモジュールMにおいて、この拡張は無効化されないが制限を受ける。Mは重複インスタンス宣言を定義できるが、それらはMで定義された別のインスタンス宣言としか重複できない。MをインポートするモジュールNの中、型クラス関数を使う呼び出し地点で、どのインスタンスを使うかの選択肢(つまり重複)があり、最も特殊性の高いインスタンスがM由来の場合、他の全ての選択肢もM由来でなければならない。これについての簡単な考え方は、Safeコンパイルされたモジュールで定義された重複インスタンスに関する同一生成元ポリシー(same origin policy)だと思うことである。-XDeriveDataTypeable拡張を介して提供されている)されたもののみに制限している。Typeable型クラスの手書きインスタンスは、unasfeに型変換を行うように簡単に悪用できるので、Safe Haskellでは許されていない。impdecl -> import [safe] [qualified] modid [as modid] [impspec]使われると、safeキーワード付きでインポートされるモジュールは信頼されたものでなければならず、そうでなければコンパイルエラーが発生する。このsafeインポート拡張は
-XSafe、-XTrustworthy、-XUnsafeのいずれか、あるいは対応するプラグマによって有効になる。-XSafeフラグが使われている場合、このsafeキーワードは許されるが無意味である。いずれにせよ全てのインポートが安全でなければならない。-XSafeを使ってコンパイルされているモジュールからインポートできないことを標示する。 あるモジュールが信頼されるかどうかの検査の手続きは、-fpackage-trustフラグが与えられているかどうかに依存する。どちらの場合の検査も良く似ており、-fpackage-trustの存在は、trustworthyモジュールが信頼されているとみなされるための要件を一つ追加するのみである。
-fpackage-trustが無効の場合)-XSafe付きでコンパイルされている -XTrustworthy付きでコンパイルされている 信頼を上のように定義することには問題がある。どんなモジュールでも-XTrustworthy付きでコンパイルすることができ、そうするとそれが何をするかにかかわらず信頼されるのである。これを制御するために、パッケージへの信頼についての追加的定義がある(-fpackage-trustで有効になる)。パッケージへの信頼の要点は、どのパッケージがtrustworthyモジュールを含めるかをクライアントCが明示的に言うことである。つまり、Cは、パッケージPとその作者を信頼するのでP内の-XTrustworthyを使ったモジュールを信頼する、と決める。パッケージへの信頼が有効の場合、trustworthyだとみなされるものの信頼されていないパッケージにあるモジュールは信頼されていないとみなされる。より形式的な定義は次の節で与えられる。
-fpackage-trustが有効の場合)-fpackage-trustが有効なら、あるモジュールが信頼されるかどうかは、パッケージへの信頼という概念に依存する。これはGHCを起動するクライアントC(つまり、あなたである)によって決定される。パッケージPが信頼されるのは、以下のどれかが成立する場合である。
どちらの場合でも、パッケージの信頼に関して決定権はCにしかない。どのパッケージを信頼するかはクライアントによる。
-fpackage-trustフラグが使われているとき、パッケージPのモジュールMがクライアントCに信頼されるのは、以下の条件が満されるとき、かつそのときに限る。
-XSafe付きでコンパイルされている -XTrustworthy付きでコンパイルされている 一番目の信頼の定義については、信頼の保証は、safe languageによって課される制約を通してGHCによって与えられる。二番目の定義については、この保証はまずモジュール作者によって与えられる。次にクライアントCが、このモジュールを含むパッケージを信頼することを示すことによって、モジュール作者への信頼を明示する。この信頼の連鎖が必要なのは、-XTrustworthyでコンパイルされたモジュールに関してGHCはいかなる保証も提供しないからである。
信頼検査に二つのモードがある理由は、-fpackage-trustによって有効になる追加の要件が、Safe Haskellの設計を侵襲的なものにするからである。このフラグが有効な場合、Safe Haskellを使っているパッケージは、ユーザの機械上のパッケージの信頼状態によってコンパイルが通ったり通らなかったりする。パッケージfooのメンテナが、セキュリティ意識のあるHaskellerがfooを使えるようにSafe Haskellを使うと、Safe Haskellを知りも構いもしないユーザに、fooが必要とするパッケージbarが彼らの機械上で信頼されていないために、fooがコンパイルできないと文句を言われることになる。この意味で、-fpackage-trustはSafe Haskellを正式に有効にするフラグだと思うことができる。これなしでは、Safe Haskellはこっそりとしたやりかたで動作する。
Having the -fpackage-trust flag also nicely unifies the semantics of how Safe Haskell works when used explicitly and how modules are inferred as safe.(訳注: 訳せません。助けて)
パッケージWuggle:
{-# LANGUAGE Safe #-}
module Buggle where
import Prelude
f x = ...blah...
パッケージP:
{-# LANGUAGE Trustworthy #-}
module M where
import System.IO.Unsafe
import safe BuggleクライアントCがパッケージPを信頼することを決めたとしよう。ではCはモジュールMを信用するか?これを決めるために、GHCはMのインポートを検査しなければいけない。MはSystem.IO.Unsafeをインポートしている。Mは-XTrustworthy付きでコンパイルされているから、このインポートについてはMの作者が責任を負う。CはPの作者を信頼しているので、CはMがunsafeなインポートを安全かつMが露出するAPIに整合的な形でしか使わないということを信頼している。Mには、BuggleへのSafeインポートもある。safeインポートなのでPの作者はこれの安全性について責任を負わないため、BuggleがCに信頼されているかをGHCが検査しないといけない。どうか?うーん、これは-XSafe付きでコンパイルされているので、Buggle自体のコードは問題ないことが機械的に検査されている。ただしこれもBuggleのインポートが全てCに信頼されていることが条件である。Preludeはbase由来であり、Cはこれを信頼しており、-XTrustworthy付きでコンパイルされている。(Preludeは典型的には暗黙にインポートされるが、その場合でもここで述べた規則に沿う)。よってBuggleは信頼されているとみなされる。
CはパッケージWuggleを信頼する必要がなかったことに注意。機械による検査だけで十分である。Cは、-XTrustworthyなモジュールを含むパッケージだけを信頼すれば良い。
-XTrustworthyを使うモジュールMの作者は、Mの公開API(エクスポートリストによって露出されているシンボル群)が安全でない方法で使えないことを保証するべきである。これは、エクスポートされているシンボルが型安全性と参照透明性に従うべきだということである。 -XSafe、-XTrustworthy、-XUnsafeのどれもが使われずにモジュールがコンパイルされる場合、GHCはそのモジュールが安全だとみなせるかを自分で見出そうとする。この安全性の推論はモジュールを決してtrustworthyだとは標示せず、unsafeかsafeだとする。これをモジュールMについて決定するために、GHCは以下の単純な方法を使う。すなわち、Mが-XSafeフラグの下でもエラーなしでコンパイルできるなら、Mはsafeであると標示する。Mが-XSafeフラグの下ではコンパイルに失敗するなら、unsafeであると標示する。
Safe Haskellの推論を使うべきなのは何時で、明示的な-XSafeフラグを使うべきなのは何時か?そのモジュールが安全でなければならないという動かせない要求があるなら、後者を使うべきである。つまり、概観した利用例や、Safe Haskellが意図している目的、すなわち信頼されていないコードをコンパイルする場合である。安全性の推論はふつうのHaskellプログラマに使われることを意図している。通常Safe Haskellを気にかけないユーザである。
Haskellライブラリを書いているとしよう。その場合、あなたは単に安全性推論を使いたいと思うだろう。安全でない言語機能を避けるなら、あなたのモジュールはsafeと標示される。これは良いことである。なぜなら、あなたのライブラリを信頼されないコードに露出されるAPIの一部として使いたいユーザが、ライブラリを変更なしで使えるからである。安全性の推論がなかったら、ライブラリの書き手が明示的にSafe Haskellを使わなければならないだろうが、これをHaskellコミュニティ全体に期待するのは合理的でない。そうでなければ、ライブラリのユーザがあなたのAPIをtrustworthyモジュールから再エクスポートするだけのラッパを書かなければならないが、これは鬱陶しい作業である。
-XTrustworthyはどのようなHaskellプログラムが受理されるかについて、およびそれらの意味について影響を与えない。例外は、safeインポートキーワードを許すことである。 -fpackage-trustが有効の場合) — されるが、モジュールのあるパッケージが信頼されている場合に限る。-XSafeでコンパイルされたモジュールからインポートできないようにする。またsafeインポート拡張を有効にし、依存関係が信頼されていることをモジュールが要求できるようにする。 GHCには、コンパイル時に任意のプロセスを走らせることを許すフラグが豊富にある。一つの例はプリプロセッサの変更フラグである。また、IOアクションを含む任意のHaskellコードをコンパイル時に走らせる能力のあるTemplate Haskellもそうである。Safe Haskellはこの危険性に対処しない(Template Haskellは禁止される機能だが)。
よって、信頼できないコードを、人力での検査なしでコンパイルする場合、以下の予防策を取ることが提案される。
-XSafeをコマンド行で指定してコンパイルする。コマンド行のフラグはソースレベルのプラグマよりも優先されるので、これによって、コンパイル対象のソースファイルの改変によってSafe Languageを無効にすることができないようになる。-fpackage-trustを使い、信頼できない供給元のパッケージが信頼されていないとみなされているようにする。GHC Wikiには、コンパイル時安全性に関する問題点と、その解決策の可能性についてのさらに詳細な議論がある。