Safe Haskellは、バージョン7.2でGHCに実装された言語拡張である。これは、コードが使うことが許されるGHC Haskellの機能を限定することによって、unsafeなコードを信頼されたコードベースに安全な形で含めることができるようにする。単純化して言うと、これはプログラム内の型を信頼に足るものにする。Safe Haskell自体は最小限のものであることを目標としていて、その上により高度な安全システム(例えば情報フロー制御によるセキュリティや、暗号化計算)を構築するのに必要十分な強さの保証を提供する。
Safe Haskellの設計は以下の面にわたっている。IOモナドを通した純粋な関数と作用のある関数の分離を提供している。しかし、この型システムには抜け穴がいくつかある。最も分かり易いのは関数unsafePerformIO :: IO a -> aである。Safe Haskellのsafe language方言はこのような関数の利用を禁止する。これによってHaskellコードを分析したり論証したりし易くなるので、色々の目的のために便利である。また、このようなunsafeな関数はどうしても必要でない限り使うべきでないという既存のHaskellコミュニティの文化を成文化するものでもある。こういう訳で、(-XSafeを使った)safe languageの利用は、-Wallの機能と似て、良いスタイルを強制する手段だととらえることができる。
Safe Haskellは、コンパイルされたコードの性質に関して、Haskellを使ってセキュアなシステムを構築するのに十分なだけの保証を与えるように設計されている。情報フロー制御、capability-basedセキュリティ、暗号化されたデータを扱うためのDSL、といったシステムをHaskellで構築するための作業が大量に為されてきた。これらのシステムは全て、unsafePerformIOのような関数が許される一般の場合には成り立たないような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を使えるのである。このため、Trustworthyとされたモジュールが信頼され、-XSafeでコンパイルされるコードから使うことを許されるためには、コードをコンパイルしているクライアントCが、このTrustworthyとされたモジュールが所属するパッケージを信頼する旨をGHCに伝えなければならない。これは実質的に、「このパッケージは-XSafeでコンパイルされる信頼されないモジュールによって使えるTrustworthyモジュールを含んでいるが、私はこのパッケージの作者を信頼し、このモジュールが安全なAPIしか露出しないことを信頼する」と言う手段をCに与える。パッケージへの信頼はいつでも変えられるので、もしパッケージに脆弱性が見付かったら、Cはそのパッケージが信頼されないと宣言し、そのパッケージに対する将来のいかなるコンパイルも失敗するようにできる。この機構についてのより詳細な概観は7.20.4. 信頼を見よ。
以上により、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 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、-XSafeImportsのいずれか、あるいは対応するプラグマによって有効になる。-XSafeフラグが使われている場合、このsafeキーワードは許されるが無意味である。いずれにせよ全てのインポートが安全でなければならない。
あるモジュールが信頼されるかどうかは、パッケージへの信頼という概念に依存する。これはGHCを起動するクライアントC(つまり、あなたである)によって決定される。パッケージPが信頼されるのは、CのパッケージデータベースのレコードがPが信頼されていると記録している(しかも、コマンド行引数によって再定義されていない)場合、あるいはCのコマンド行フラグが、パケージデータベースの記録に関係なく信頼しろと言っている場合である。どちらの場合でも、パッケージの信頼に関して決定権はCにしかない。どのパッケージを信頼するかはクライアントによる。
よって、パッケージPのモジュールMがクライアントCに信頼されるのは、以下の条件が満されるとき、かつそのときに限る。-XSafe付きでコンパイルされている
-XTrustworthy付きでコンパイルされている
-XTrustworthyでコンパイルされたモジュールに関してGHCはいかなる保証も提供しないからである。
パッケージ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なモジュールを含むパッケージだけを信頼すれば良い。
import safe Untrusted.Module
safeインポートキーワードはSafe Haskellの機能であってHaskell98ではないので、Safe Haskelの言語フラグのどれかを指定しないとこれは失敗する。safeインポートを有効にするフラグは-XSafe、-XTrustworthy、-XSafeImportsの三つである。しかし、-XSafeと-XTrustworthyは単にこのキーワードを有効にする以上のことをするので、望ましくないことがある。-XSafeImportsを使うとsafeインポートを有効にしつつ、その他にはなにもしないということができる。
-XTrustworthyはどのようなHaskellプログラムが受理されるかについて、およびそれらの意味について影響を与えない。例外は、safeインポートキーワードを許すことである。