Безопасный Haskell — это расширение языка Haskell, реализованное в GHC начиная с версии 7.2. Оно позволяет безопасно включать небезопасный код в доверенную базу кода, ограничивая возможности GHC Haskell, используемые этим кодом. Проще говоря, он делает типы программ доверенными.
Хотя основное применение Безопасного Haskell — это выполнение недоверенного кода, Безопасный Haskell не предоставляет это напрямую. Вместо этого, Безопасный Haskell обеспечивает строгую типовую безопасность. Без Безопасного Haskell GHC допускает множество исключений из системы типов, которые могут нарушить любые абстракции. Обеспечивая строгую типовую безопасность, Безопасный Haskell позволяет разработчикам создавать собственные механизмы песочницы на уровне библиотек для выполнения недоверенного кода.
Хотя Безопасный Haskell является расширением, он фактически работает в фоновом режиме для каждой компиляции с GHC. Он делает это для отслеживания нарушений типов модулей, чтобы определить их безопасность, даже если они явно не используют Безопасный Haskell. Подробнее об этом см. в разделе Вывод Безопасного Haskell.
Конструкции Безопасного Haskell охватывают следующие аспекты:
- Диалект языка Haskell безопасный язык, обеспечивающий более строгие гарантии относительно кода. Он позволяет доверять типам и границам модулей.
- Расширение безопасного импорта, указывающее, что импортируемый модуль должен быть доверенным.
- Определение доверия (или безопасности) и его работы, а также способы определения и изменения доверия к модулям и пакетам.
Однако Безопасный Haskell не предлагает компиляционной безопасности. В процессе компиляции возможно запускать произвольные процессы, например, используя флаг настраиваемого препроцессора. Это можно использовать для компрометации системы пользователя во время компиляции или для изменения исходного кода непосредственно перед компиляцией, чтобы попытаться изменить флаги Безопасного Haskell. Это обсуждается более подробно в разделе Безопасная компиляция.
6.18.1. Использование Безопасного Haskell
Безопасный Haskell был разработан с учетом двух вариантов использования:
- Принуждение к строгой типовой безопасности во время компиляции
- Компиляция и выполнение недоверенного кода
6.18.1.1. Строгая типовая безопасность (хороший стиль)
Haskell предлагает мощную систему типов и разделение чистых и эффектных функций через IO монаду. Однако в системе типов есть несколько лазеек, наиболее очевидной из которых является функция unsafePerformIO :: IO a -> a. Диалект безопасного языка Безопасного Haskell запрещает использование таких функций. Это может быть полезным ограничением, так как делает код Haskell проще для анализа и обоснования. Это также кодифицирует существующую культуру в сообществе Haskell по избеганию небезопасных функций, если это абсолютно необходимо. Таким образом, использование безопасного языка (через флаг -XSafe) можно рассматривать как способ обеспечения хорошего стиля, аналогично функции -Wall.
6.18.1.2. Создание безопасных систем (ограниченные монады ввода/вывода)
Системы, такие как безопасность контроля потока информации, системы безопасности на основе возможностей и DSL для работы с зашифрованными данными и т.д., могут быть созданы на языке Haskell в виде библиотеки. Однако они требуют гарантий о свойствах Haskell, которые неверны в общем случае из-за наличия функций, таких как unsafePerformIO. Безопасный Haskell предоставляет пользователям достаточные гарантии системы типов, чтобы позволить им создавать такие безопасные системы.
Например, давайте определим интерфейс системы плагинов, где авторы плагинов являются недоверенными, возможно, злонамеренными третьими лицами. Мы делаем это, ограничивая интерфейс плагина чистыми функциями или ограниченной IO монадой, которую мы определили. Ограниченная IO монада позволит выполнять только безопасный подмножество IO действий. Мы определяем интерфейс плагина таким образом, что он требует от модуля плагина Danger, экспортировать одно вычисление Danger.runMe, типа RIO (), где RIO — это монада, определённая следующим образом:
-- While we use `Safe', the `Trustworthy' pragma would also be
-- fine. We simply want to ensure that:
-- 1) The module exports an interface that untrusted code can't
-- abuse.
-- 2) Untrusted code can import this module.
--
{-# LANGUAGE Safe #-}
module RIO (RIO(), runRIO, rioReadFile, rioWriteFile) where
-- Notice that symbol UnsafeRIO is not exported from this module!
newtype RIO a = UnsafeRIO { runRIO :: IO a }
instance Functor RIO where
fmap f (UnsafeRIO m) = UnsafeRIO (fmap f m)
instance Applicative RIO where
pure = UnsafeRIO . pure
(UnsafeRIO f) <*> (UnsafeRIO m) = UnsafeRIO (f <*> m)
instance Monad RIO where
(UnsafeRIO m) >>= k = UnsafeRIO $ m >>= runRIO . k
-- Returns True iff access is allowed to file name
pathOK :: FilePath -> IO Bool
pathOK file = {- Implement some policy based on file name -}
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 с помощью нового флага Безопасного Haskell -XSafe:
{-# LANGUAGE Safe #-}
module Danger ( runMe ) where
runMe :: RIO ()
runMe = ...
Прежде чем перейти к деталям Безопасного Haskell, давайте отметим некоторые причины, по которым этот механизм безопасности потерпит неудачу без Безопасного Haskell:
- Этот дизайн пытается ограничить операции, которые может выполнить
Danger, используя типы, в частности обертку типаRIOнадIO. Однако авторDangerможет обойти это, просто написав произвольныеIOдействия и используяunsafePerformIO :: IO a -> aдля их выполнения как чистых функций. - Этот дизайн также полагается на
Dangerне имея возможности получить доступ к конструкторуUnsafeRIO. К сожалению, Template Haskell может использоваться для обхода границ модулей, а значит, и для получения доступа к этому конструктору. - Нет способа ограничить модули, которые может импортировать
Danger. Это дает авторуDangerочень большую поверхность атаки, по существу, любой пакет, установленный в системе. Если какой-либо из этих пакетов имеет уязвимость, то модульDangerможет использовать ее.
Безопасный Haskell предотвращает все эти атаки. Это делается путём компиляции модуля RIO с флагом Safe или Trustworthy и компиляции Danger с флагом Safe. Мы объясним каждый из них ниже.
Использование Safe для компиляции Danger ограничивает возможности Haskell, которые могут использоваться, до безопасного подмножества. Это включает запрет unsafePerformIO, Template Haskell, чистых функций FFI, правил и ограничение работы перекрывающихся экземпляров. Флаг Safe также ограничивает модули, которые может импортировать Danger, только теми, которые считаются доверенными. Доверенные модули — это модули, скомпилированные с флагом Safe, где GHC предоставляет механическую гарантию безопасности кода. Или модули, скомпилированные с флагом Trustworthy, где автор модуля утверждает, что модуль безопасен.
Вот почему модуль RIO компилируется с флагом Safe или Trustworthy, чтобы позволить модулю Danger импортировать его. Флаг Trustworthy не накладывает никаких ограничений на модуль, как Safe (кроме ограничения перекрывающихся экземпляров до безопасных перекрывающихся экземпляров). Вместо этого автор модуля заявляет, что, хотя код может использовать небезопасные функции внутри, он экспонирует только API, который может использоваться безопасным способом.
Однако неограниченное использование флага Trustworthy является проблемой, поскольку произвольный модуль может использовать его, чтобы пометить себя как доверенный, но Trustworthy не предоставляет никаких гарантий о модуле, в отличие от Safe. Для контроля использования доверенных модулей рекомендуется использовать флаг -fpackage-trust. Этот флаг добавляет дополнительное требование к проверке доверия для доверенных модулей. Он требует, чтобы для того, чтобы доверенный модуль считался доверенным и мог использоваться в коде, скомпилированном с флагом Safe, клиент C, компилирующий код, должен сообщить GHC, что он доверяет пакету, в котором находится доверенный модуль. Это по существу способ сказать C, что, хотя этот пакет содержит доверенные модули, которые могут быть использованы недоверенными модулями, скомпилированными с Safe, я доверяю автору(ам) этого пакета и доверяю, что модули экспонируют только безопасный API. Доверие к пакету может быть изменено в любое время, поэтому, если в пакете найдена уязвимость, C может объявить пакет недоверенным, чтобы любая последующая компиляция против этого пакета завершилась неудачей. Более подробный обзор этого механизма см. в разделе Доверие и режимы Безопасного Haskell.
В примере Danger может импортировать модуль RIO, потому что RIO скомпилирован с флагом Safe. Таким образом, Danger может использовать функции rioReadFile и rioWriteFile для доступа к разрешенным именам файлов. Основное приложение затем импортирует как RIO, так и Danger. Для запуска плагина оно вызывает RIO.runRIO Danger.runMe в монаде IO. Приложение находится в безопасности, зная, что единственные IO будут касаться файлов, пути к которым были одобрены тестом pathOK.
Проверки Безопасного Haskell можно отключить для модуля, передав флаг -fno-safe-haskell. Это особенно полезно при компиляции с плагинами исходного кода, так как запуск плагина отмечает модуль как небезопасный и может вызвать неудачу проверок безопасности в последующих модулях.