Safe Haskell — это расширение языка Haskell, реализованное в GHC начиная с версии 7.2. Оно позволяет безопасно включать небезопасный код в доверенную кодовую базу, ограничивая возможности GHC Haskell, которые разрешено использовать этому коду. Проще говоря, оно позволяет доверять типам программ.
Хотя основной вариант использования Safe Haskell — запуск недоверенного кода, Safe Haskell не обеспечивает этого напрямую. Вместо этого Safe Haskell обеспечивает строгую безопасность типов. Без Safe Haskell GHC допускает множество исключений из системы типов, которые могут нарушить любые абстракции. Обеспечивая строгую безопасность типов, Safe Haskell позволяет разработчикам создавать собственные механизмы изоляции на уровне библиотек для запуска недоверенного кода.
Хотя Safe Haskell является расширением, оно фактически работает в фоновом режиме при каждой компиляции с помощью GHC. Это позволяет отслеживать нарушения системы типов в модулях и определять их безопасность, даже если они явно не используют Safe Haskell. Дополнительные сведения см. в разделе Вывод о безопасности в Safe Haskell.
При разработке Safe Haskell учитывались следующие аспекты:
- безопасный язык — диалект Haskell, обеспечивающий более строгие гарантии относительно кода. Он позволяет доверять типам и границам модулей.
- Расширение безопасного импорта, указывающее, что импортируемому модулю необходимо доверять.
- Определение доверия (или безопасности) и принципов его действия, а также способы определения и изменения уровня доверия к модулям и пакетам.
Однако Safe Haskell не обеспечивает безопасность компиляции. Во время компиляции можно запускать произвольные процессы, например, используя флаг пользовательского препроцессора. Это можно использовать для компрометации системы пользователя во время компиляции или для изменения исходного кода непосредственно перед компиляцией, чтобы попытаться изменить флаги Safe Haskell. Более подробно это обсуждается в разделе Безопасная компиляция.
6.18.1. Применение Safe Haskell
Safe Haskell разрабатывался с учетом двух вариантов использования:
- Обеспечение строгой безопасности типов во время компиляции
- Компиляция и выполнение недоверенного кода
6.18.1.1. Строгая безопасность типов (хороший стиль)
Haskell предлагает мощную систему типов и разделение чистых и эффектных функций с помощью монады IO. Однако в системе типов есть несколько лазеек, самая очевидная из которых — функция unsafePerformIO :: IO a -> a. Диалект безопасного языка Safe Haskell запрещает использование подобных функций. Это полезное ограничение, поскольку оно упрощает анализ кода Haskell и рассуждения о нем. Оно также закрепляет существующую в сообществе Haskell практику избегать небезопасных функций, если только в них нет крайней необходимости. Поэтому использование безопасного языка (с помощью флага -XSafe) можно считать способом обеспечения хорошего стиля, аналогичным назначению -Wall.
6.18.1.2. Создание защищенных систем (ограниченные монады IO)
Такие системы, как средства обеспечения безопасности контроля информационных потоков, системы безопасности на основе возможностей и DSL для работы с зашифрованными данными и т. д., можно реализовать в Haskell в виде библиотеки. Однако для этого требуются гарантии относительно свойств Haskell, которые в общем случае не выполняются из-за наличия таких функций, как unsafePerformIO. Safe 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 с помощью нового флага Safe Haskell -XSafe:
{-# LANGUAGE Safe #-}
module Danger ( runMe ) where
runMe :: RIO ()
runMe = ...
Прежде чем перейти к подробностям Safe Haskell, перечислим некоторые причины, по которым этот механизм безопасности не сработал бы без Safe Haskell:
- В этой конструкции предпринимается попытка ограничить операции, которые может выполнять
Danger, с помощью типов, в частности оболочки типаRIOвокругIO. Однако авторDangerможет обойти это ограничение, просто написав произвольные действияIOи используяunsafePerformIO :: IO a -> aдля их выполнения в виде чистых функций. - Конструкция также предполагает, что
Dangerне сможет получить доступ к конструкторуUnsafeRIO. К сожалению, Template Haskell можно использовать для обхода границ модулей и получения доступа к этому конструктору. - Невозможно ограничить список модулей, которые может импортировать
Danger. Это предоставляет авторуDangerочень широкую поверхность для атаки, включающую практически любой пакет, установленный в системе. Если в каком-либо из этих пакетов есть уязвимость, модульDangerсможет ее использовать.
Safe Haskell предотвращает все эти атаки. Для этого модуль RIO компилируется с флагом Safe или Trustworthy, а Danger компилируется с флагом Safe. Ниже мы объясним каждый из них.
Использование Safe для компиляции Danger ограничивает доступные возможности Haskell безопасным подмножеством (safe subset). Это включает запрет на unsafePerformIO, Template Haskell, чистые функции FFI и RULES, а также ограничение работы перекрывающихся экземпляров. Флаг Safe также ограничивает список модулей, которые может импортировать Danger, только теми, которые считаются доверенными. Доверенными являются модули, скомпилированные с помощью Safe, для которых GHC механически гарантирует безопасность кода, либо модули, скомпилированные с помощью Trustworthy, автор которых заявляет, что модуль безопасен.
Именно поэтому модуль RIO компилируется с помощью Safe или Trustworthy, чтобы модуль Danger мог его импортировать. Флаг Trustworthy, в отличие от Safe, не накладывает на модуль никаких ограничений (кроме ограничения на перекрывающиеся экземпляры, которые должны быть безопасными перекрывающимися экземплярами). Вместо этого автор модуля заявляет, что, хотя внутри код может использовать небезопасные возможности, он предоставляет API, которым можно безопасно пользоваться.
Однако свободное использование Trustworthy представляет проблему: произвольный модуль может использовать его, чтобы объявить себя доверенным, хотя Trustworthy не дает никаких гарантий относительно модуля, в отличие от Safe. Для контроля использования заслуживающих доверия модулей рекомендуется использовать флаг -fpackage-trust. Этот флаг добавляет дополнительное условие при проверке доверия к заслуживающим доверия модулям. Чтобы такие модули считались доверенными и могли использоваться в коде, скомпилированном с помощью Safe, клиент C, компилирующий код, должен сообщить GHC, что доверяет пакету, в котором находится заслуживающий доверия модуль. По сути, это способ для C заявить: хотя этот пакет содержит заслуживающие доверия модули, которые могут использоваться недоверенными модулями, скомпилированными с помощью Safe, я доверяю автору или авторам этого пакета и верю, что модули предоставляют только безопасный API. Уровень доверия к пакету можно изменить в любой момент: поэтому, если в пакете обнаружена уязвимость, C может объявить пакет недоверенным, и любая последующая компиляция с использованием этого пакета завершится ошибкой. Более подробное описание этого механизма см. в разделе Доверие и режимы Safe Haskell.
В этом примере Danger может импортировать модуль RIO, поскольку RIO скомпилирован с помощью Safe. Таким образом, Danger может использовать функции rioReadFile и rioWriteFile для доступа к разрешенным именам файлов. Затем основное приложение импортирует и RIO, и Danger. Чтобы запустить плагин, оно вызывает RIO.runRIO Danger.runMe в монаде IO. Приложение может быть уверено, что единственные возникающие IO будут относиться к файлам, пути к которым одобрены проверкой pathOK.
Проверки Safe Haskell можно отключить для модуля, передав флаг -fno-safe-haskell. Это особенно полезно при компиляции с плагинами исходного кода: запуск плагина помечает модуль как небезопасный и может привести к тому, что последующие модули не пройдут проверки безопасности.
6.18.2. Безопасный язык
Безопасный язык Safe Haskell (включаемый с помощью -XSafe) гарантирует следующие свойства:
-
Референциальная прозрачность — Типам можно доверять. Любая чистая функция гарантированно является чистой. Ее вычисление детерминировано и не вызывает побочных эффектов. Функции в монаде
IOпо-прежнему разрешены и ведут себя как обычно. Например, функцияunsafePerformIO :: IO a -> aзапрещена в безопасном языке, чтобы обеспечить это свойство. -
Контроль границ модулей — В безопасном языке доступ возможен только к символам, общедоступным через списки экспорта других модулей. Значения, использующие конструкторы данных, не экспортированные определяющим модулем, нельзя исследовать или создавать. Поэтому, если модуль
Mзадает некоторые инварианты благодаря тщательно составленному списку экспорта, код, написанный на безопасном языке и импортирующийM, гарантированно будет соблюдать эти инварианты. - Семантическая согласованность — Для любого модуля, импортирующего модуль, написанный на безопасном языке, выражения, компилирующиеся как с безопасным импортом, так и без него, имеют в обоих случаях одинаковый смысл. Иными словами, импорт модуля, написанного на безопасном языке, не может изменить смысл существующего кода, не зависящего от этого модуля. Например, на использование OverlappingInstances накладываются определенные ограничения, поскольку оно может нарушить это свойство.
- Строгое подмножество — Безопасный язык является строгим подмножеством Haskell в реализации GHC. Любое выражение, компилирующееся на безопасном языке, имеет тот же смысл, что и при компиляции в обычном Haskell.
Эти четыре свойства гарантируют, что в безопасном языке можно доверять типам, полагаться на соблюдение списков экспорта модулей и быть уверенным, что успешно скомпилированный код имеет тот же смысл, что и обычно.
Для обеспечения этих свойств в диалекте безопасного языка полностью отключены следующие возможности:
-
TemplateHaskell— Может использоваться для получения доступа к конструкторам и абстрактным типам данных, не экспортированным модулем, что позволяет обходить границы модулей.
Кроме того, следующие возможности ограничены:
-
ForeignFunctionInterface— Запрещены объявления внешнего импорта функций, имеющих тип, отличный отIO. -
RULES— Правила переписывания, определенные в модуле M, скомпилированном с помощьюSafe, отбрасываются. Правила, определенные в заслуживающих доверия модулях, импортируемых модулемM, по-прежнему действуют и применяются как обычно. -
OverlappingInstances— Создание перекрывающихся экземпляров не ограничено, но их использование в конкретном месте вызова ограничено. Это подробное ограничение; см. Безопасные перекрывающиеся экземпляры. -
GeneralisedNewtypeDerivingиDerivingVia— GND и DerivingVia запрещены в безопасном языке. Это связано с тем, что они могут нарушать границы модулей, если авторы модулей забывают при необходимости указывать для своих типов номинальные роли. По этой причине модульData.Coerceтакже считается небезопасным. Мы надеемся найти более удачное решение в будущем. -
GHC.Generics— В Safe Haskell запрещены вручную написанные экземпляры класса типовGeneric. Такие экземпляры не являются строго небезопасными, но существует важный инвариант: экземплярGenericдолжен соответствовать структуре типа данных, для которого он определен. Разрешение вручную реализованных экземпляровGenericнарушило бы этот инвариант. Производные экземпляры (с помощью расширенияDeriveGeneric) по-прежнему разрешены. Обратите внимание, что единственная стратегия вывода, разрешенная для выводаGenericв Safe Haskell, — этоstock, поскольку другая стратегия (например,anyclass) создала бы экземпляр, нарушающий инвариант.Дополнительные сведения см. в разделе обобщенное программирование.
6.18.2.1. Безопасные перекрывающиеся экземпляры
Из-за гарантии семантической согласованности Safe Haskell необходимо ограничить работу перекрывающихся экземпляров. Мы не ограничиваем возможность их определения, поскольку это глобальное свойство, которое нельзя определить, анализируя один модуль. Вместо этого при вызове модулем функции, принадлежащей классу типов, мы проверяем, считается ли выполненное разрешение экземпляра «безопасным». Эта проверка применяется к модулям, скомпилированным как с -XSafe, так и с -XTrustworthy.
Рассмотрим следующие модули:
{-# LANGUAGE Safe #-}
module Class (TC(..)) where
class TC a where { op :: a -> String }
{-# LANGUAGE Safe #-}
module Dangerous (TC(..)) where
import Class
instance
{-# OVERLAPS #-}
TC [Int] where { op _ = "[Int]" }
{-# LANGUAGE Safe #-}
module TCB_Runner where
import Class
import Dangerous
instance
TC [a] where { op _ = "[a]" }
f :: String
f = op ([1,2,3,4] :: [Int])
И модуль Class, и модуль Dangerous скомпилируются с помощью Safe без проблем. Однако в модуле TCB_Runner необходимо проверить, безопасен ли вызов op в функции f.
Что значит «безопасный»? Импорт модуля, скомпилированного с помощью Safe, не должен менять смысл кода, который успешно компилируется без импорта этого модуля. Это свойство Safe Haskell называется семантической согласованностью.
В нашем случае модуль TCB_Runner успешно компилируется без импорта модуля Dangerous. Поэтому при выборе экземпляра для вызова op небезопасной будет ситуация, в которой экземпляр TC [Int] из модуля Dangerous окажется наиболее специализированным. Это не позволяет написанному сторонними разработчиками, которым мы не доверяем, коду (скомпилированному с помощью -XSafe в Safe Haskell) менять поведение уже существующего кода.
В частности, для определения того, является ли вызов метода класса типов небезопасным при наличии перекрывающихся экземпляров, применяется следующее правило:
- Наиболее специализированный экземпляр
Ixопределен в скомпилированном модуле-XSafe. -
Ix— осиротевший экземпляр или экземпляр многопараметрического класса типов. -
По крайней мере один перекрываемый экземпляр
Iyодновременно удовлетворяет следующим условиям:- Он находится в модуле, отличном от
Ix -
Iyне помечен какOVERLAPPABLE
- Он находится в модуле, отличном от
Это довольно сложная эвристика, но она охватывает ситуацию, когда импортированный модуль N меняет поведение существующего кода. Например, если второе условие не нарушено, автор модуля M должен зависеть либо от класса типов, либо от типа, определенного в N.
Если вызов метода класса типов считается небезопасным из-за перекрывающихся экземпляров и компилируемый модуль использует Safe или Trustworthy, компиляция завершится ошибкой. Для Unsafe ограничение не применяется, а модули, для которых используется вывод о безопасности, будут признаны небезопасными.
6.18.3. Безопасный импорт
Safe Haskell добавляет небольшое расширение к обычному синтаксису импорта Haskell — ключевое слово safe:
impdecl -> import [safe] [qualified] modid [as modid] [impspec]
При использовании этого ключевого слова импортируемый модуль должен быть доверенным, иначе произойдет ошибка компиляции. Расширение безопасного импорта включается одним из флагов: -XSafe, -XTrustworthy или -XUnsafe. При использовании флага -XSafe ключевое слово safe разрешено, но не имеет смысла, поскольку каждый импорт рассматривается как безопасный.