10.40. Безопасный Haskell
Безопасный Haskell — это расширение языка Haskell, реализованное в GHC начиная с версии 7.2. Оно позволяет безопасно включать небезопасный код в надёжный код-базис, ограничивая доступные функции GHC Haskell. Проще говоря, он делает типы программ надёжными.
Хотя основное применение Безопасного Haskell — выполнение ненадежного кода, он не предоставляет это напрямую. Вместо этого Безопасный Haskell обеспечивает строгую типизацию. Без Безопасного Haskell GHC допускает множество исключений из системы типов, которые могут нарушить любые абстракции. Предоставляя строгую типизацию, Безопасный Haskell позволяет разработчикам создавать собственные механизмы песочниц на уровне библиотек для выполнения ненадежного кода.
Хотя Безопасный Haskell — это расширение, он фактически работает в фоновом режиме для каждой компиляции с помощью GHC. Он делает это, чтобы отслеживать нарушения типов модулей, чтобы определить их безопасность, даже когда они явно не используют Безопасный Haskell. Подробную информацию об этом см. в разделе Вывод типов Безопасного Haskell.
Дизайн Безопасного Haskell охватывает следующие аспекты:
- Диалект Haskell «безопасный язык», который обеспечивает более строгие гарантии в отношении кода. Он позволяет доверять типам и границам модулей.
- Расширение «безопасный импорт», которое указывает, что импортируемый модуль должен быть надёжным.
- Определение «надёжности» (или безопасности) и её функционирования, а также способы определения и изменения надёжности модулей и пакетов.
Однако Безопасный Haskell не гарантирует безопасность компиляции. Во время компиляции можно запускать произвольные процессы, например, используя флаг настраиваемого препроцессора. Это можно использовать для компрометации системы пользователя во время компиляции или для изменения исходного кода непосредственно перед компиляцией, чтобы попытаться изменить флаги Безопасного Haskell. Это обсуждается подробнее в разделе Безопасная компиляция.
10.40.1. Сферы применения Безопасного Haskell
Безопасный Haskell был разработан с учётом двух сфер применения:
- Принуждение к строгой типизации во время компиляции
- Компиляция и выполнение ненадежного кода
10.40.1.1. Строгая типизация (хороший стиль)
Haskell предлагает мощную систему типов и разделение чистых и эффективных функций через IO монаду. Однако в системе типов есть несколько слабых мест, наиболее очевидным из которых является функция unsafePerformIO :: IO a -> a. Диалект «безопасный язык» Безопасного Haskell запрещает использование таких функций. Это может быть полезным ограничением, так как оно упрощает анализ и обоснование кода Haskell. Оно также кодифицирует существующую культуру в сообществе Haskell по избеганию небезопасных функций, если это не абсолютно необходимо. Таким образом, использование безопасного языка (через -XSafe флаг) можно рассматривать как способ обеспечения хорошего стиля, аналогично функции -Wall.
10.40.1.2. Создание надёжных систем (ограниченные монады IO)
Системы, такие как безопасность управления потоком информации, системы безопасности на основе возможностей и 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 Monad RIO where
return = UnsafeRIO . return
(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, 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 может объявить пакет ненадежным, чтобы любая последующая компиляция с этим пакетом завершилась неудачей. Более подробный обзор этого механизма см. в Доверие и режимы Безопасного Haskell.
В примере Danger может импортировать модуль RIO, потому что RIO скомпилирован с Safe. Таким образом, Danger может использовать функции rioReadFile и rioWriteFile для доступа к разрешённым именам файлов. Основное приложение затем импортирует как RIO, так и Danger. Для запуска плагина оно вызывает RIO.runRIO Danger.runMe в пределах IO монады. Приложение безопасно в том знании, что единственным следствием будет доступ к файлам, пути к которым были одобрены тестом pathOK.
Проверки Безопасного Haskell можно отключить для модуля, передав флаг -fno-safe-haskell. Это особенно полезно при компиляции с плагинами для исходного кода, так как запуск плагина помечает модуль как небезопасный и может привести к отказу проверок безопасности в последующих модулях.
10.40.2. Безопасный язык
Безопасный язык Безопасного Haskell (включённый с помощью -XSafe) гарантирует следующие свойства:
-
Референтная прозрачность — Типы можно доверять. Любая чистая функция гарантированно является чистой. Их вычисление детерминировано и не вызовет побочных эффектов. Функции в монаде
IOвсё ещё разрешены и ведут себя как обычно. Таким образом, например, функцияunsafePerformIO :: IO a -> aзапрещена в безопасном языке для обеспечения этого свойства. -
Контроль границ модулей — В безопасном языке можно получить доступ только к символам, которые доступны публично через другие списки экспорта модулей. Значения, использующие конструкторы данных, не экспортированные определяющим модулем, не могут быть исследованы или созданы. Таким образом, если модуль
Mустанавливает некоторые инварианты с помощью тщательного использования своего списка экспорта, то код, написанный на безопасном языке и импортирующийM, гарантированно будет соблюдать эти инварианты. - Семантическая согласованность — Для любого модуля, импортирующего модуль, написанный на безопасном языке, выражения, которые компилируются как с безопасным импортом, так и без него, имеют одинаковый смысл в обоих случаях. То есть, импорт модуля, написанного на безопасном языке, не может изменить значение существующего кода, который не зависит от этого модуля. Например, существуют некоторые ограничения на использование Перекрывающихся инстансов, так как они могут нарушать это свойство.
- Строгое подмножество — Безопасный язык строго является подмножеством языка Haskell, реализованного GHC. Любое выражение, которое компилируется на безопасном языке, имеет тот же смысл, что и при компиляции в обычном Haskell.
Эти четыре свойства гарантируют, что в безопасном языке вы можете доверять типам, можете доверять, что списки экспорта модулей соблюдаются, и можете доверять, что код, который успешно скомпилирован, имеет тот же смысл, что и обычно.
Для достижения этих свойств в диалекте безопасного языка мы полностью отключаем следующие функции:
-
TemplateHaskell— Может использоваться для получения доступа к конструкторам и абстрактным типам данных, которые не были экспортированы модулем, нарушая границы модулей.
Кроме того, мы ограничиваем следующие функции:
-
ForeignFunctionInterface— Объявления внешнего импорта, импортирующие функцию с типом, отличным отIO, запрещены. -
RULES— Правила переписывания, определенные в модуле M, скомпилированном сSafe, отбрасываются. Правила, определенные в надёжных модулях, которыеMимпортирует, всё ещё действительны и будут срабатывать как обычно. -
OverlappingInstances— Нет ограничений на создание перекрывающихся инстансов, но мы ограничиваем их использование в конкретной точке вызова. Это подробное ограничение, для получения подробностей обратитесь к разделу Безопасные перекрывающиеся инстансы. -
GeneralisedNewtypeDeriving— GND не разрешен в безопасном языке. Это связано с возможностью нарушения границ модулей, когда авторы модулей забывают разместить номинальные аннотации ролей на своих типах как нужно. По этой причине модульData.Coerceтакже считается небезопасным. Мы надеемся найти лучшее решение в будущем. -
GHC.Generics— Рукописные инстансы класса типовGenericне разрешены в Safe Haskell. Такие инстансы не являются строго небезопасными, но существует важный инвариант, что инстансGenericдолжен соответствовать структуре типа данных, для которого определён инстанс, и разрешение вручную реализованных инстансовGenericнарушит этот инвариант. Производные инстансы (через расширениеDeriveGeneric) всё ещё разрешены. Обратите внимание, что единственная разрешённая стратегия вывода для выводаGenericв Safe Haskell — этоstock, так как другая стратегия (например,anyclass) привела бы к инстансу, нарушающему инвариант.Для получения более подробной информации см. раздел обобщённое программирование.
10.40.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 ограничений не накладывается, а для модулей с безопасным выводом они будут выведены как небезопасные.
10.40.3. Безопасные импорты
Safe Haskell вводит небольшое расширение к обычному синтаксису импорта Haskell, добавляя ключевое слово safe:
impdecl -> import [safe] [qualified] modid [as modid] [impspec]
Когда оно используется, импортируемый модуль с ключевым словом safe должен быть надёжным, в противном случае произойдёт ошибка компиляции. Расширение безопасного импорта включается с помощью флагов -XSafe , -XTrustworthy или -XUnsafe. При использовании флага -XSafe, ключевое слово safe разрешено, но бессмысленно, так как каждый импорт рассматривается как безопасный импорт.
10.40.4. Доверие и режимы Safe Haskell
Safe Haskell вводит следующие три флага языка:
-
Safe— Включает диалект безопасного языка, прося GHC гарантировать доверие. Диалект безопасного языка требует, чтобы все импорты были надёжными, иначе произойдёт ошибка компиляции. Safe Haskell также автоматически выведет этот тип безопасности для модулей, когда это возможно. Для получения более подробной информации об этом см. раздел Вывод Safe Haskell. -
Trustworthy— Означает, что, хотя этот модуль может вызывать небезопасные функции внутри себя, автор модуля заявляет, что он экспортирует API, который нельзя использовать небезопасным образом. Это не включает безопасный язык. Однако это ограничивает разрешение перекрывающихся инстансов, чтобы разрешить только безопасные перекрывающиеся инстансы. Гарантия доверия предоставляется автором модуля, а не GHC. Оператор импорта с ключевым словомsafeприводит к ошибке компиляции, если импортируемый модуль ненадёжен. Оператор импорта без ключевого слова ведёт себя как обычно и может импортировать любой модуль, будь то надёжный или нет. -
Unsafe— Помечает компилируемый модуль как небезопасный, чтобы модули, скомпилированные с помощьюSafe, не могли его импортировать. Вы можете явно пометить модуль как небезопасный, когда он экспортирует внутренние конструкторы, которые могут использоваться для нарушения инвариантов.
Хотя это флаги, они также соответствуют типам модулей Safe Haskell, которые может иметь модуль. Вы можете рассматривать их как объявление явного контракта (или типа), который должен иметь модуль. Если он неверен, компиляция завершится ошибкой. GHC также выведет правильный тип для Safe Haskell, для получения более подробной информации см. раздел Вывод Safe Haskell.
Процедура проверки того, надёжен ли модуль или нет, зависит от наличия флага -fpackage-trust. Проверка аналогична в обоих случаях, причём флаг -fpackage-trust добавляет дополнительное требование к надёжным модулям, чтобы они считались надёжными.
10.40.4.1. Проверка доверия (-fpackage-trust отключено)
Модуль M в пакете P доверяется клиенту C тогда и только тогда, когда:
-
Выполняются оба условия:
- Модуль был скомпилирован с использованием
Safe - Все прямые импорты модуля M доверяются клиенту C
- Модуль был скомпилирован с использованием
-
или выполняются все эти условия:
- Модуль был скомпилирован с использованием
Trustworthy - Все прямые безопасные импорты модуля
Mдоверяются клиенту C
- Модуль был скомпилирован с использованием
В приведенном выше определении доверия есть проблема. Любой модуль может быть скомпилирован с Trustworthy и будет считаться доверенным. Для управления этим есть дополнительное определение доверия к пакету (включенное с помощью флага -fpackage-trust). Суть доверия к пакету заключается в том, чтобы клиент C явно указал, какие пакеты разрешено содержать доверенные модули. Доверенные пакеты считаются доверенными только если они находятся в доверенных клиентом C пакетах.
10.40.4.2. Проверка доверия (-fpackage-trust включено)
Когда флаг -fpackage-trust включен, доверенность модуля зависит от того, доверен ли определённый пакет. Доверие к пакету определяется клиентом C, вызывающим GHC (то есть вами).
То есть, пакет P считается доверенным, когда выполняется одно из следующих условий:
- База данных пакетов клиента C записывает, что
Pдоверен (и никакие параметры командной строки это не переопределяют) - Флаги командной строки клиента C указывают на доверие к
Pнезависимо от того, что записано в базе данных пакетов.
В любом случае, клиент C является единственным авторитетом по доверию к пакету. Клиент сам решает, каким пакетам доверять.
При использовании флага -fpackage-trust, модуль M из пакета P доверяется клиенту C, если и только если:
-
Выполняются оба условия:
- Модуль был скомпилирован с использованием
Safe - Все прямые импорты модуля
Mдоверяются клиенту C
- Модуль был скомпилирован с использованием
-
или выполняются все эти условия:
- Модуль был скомпилирован с использованием
Trustworthy - Все прямые безопасные импорты модуля
Mдоверяются клиенту C - Пакет
Pдоверяется клиенту C
- Модуль был скомпилирован с использованием
Для первого определения доверия гарантия доверия предоставляется GHC через ограничения, наложенные безопасным языком. Для второго определения доверия гарантия предоставляется автором модуля изначально. Затем клиент C подтверждает, что доверяет автору модуля, указав, что доверяет пакету, в котором находится модуль. Эта цепочка доверия требуется, так как GHC не предоставляет гарантий для Trustworthy скомпилированных модулей.
Причина наличия двух режимов проверки доверия заключается в том, что дополнительное требование, активированное флагом -fpackage-trust, делает дизайн Safe Haskell инвазивным. Пакеты, использующие Safe Haskell, когда флаг включен, могут или не могут компилироваться в зависимости от состояния доверенных пакетов на машине пользователя. Это ненадёжно и приводит к ошибкам компиляции для всех пользователей, даже если они не пытаются использовать гарантии, предоставляемые Safe Haskell. Отключение -fpackage-trust по умолчанию и превращение его в флаг делает Safe Haskell расширением по выбору, а не всегда включённой функцией.
10.40.4.3. Пример
Package Wuggle:
{-# LANGUAGE Safe #-}
module Buggle where
import Prelude
f x = ...blah...
Package P:
{-# LANGUAGE Trustworthy #-}
module M where
import System.IO.Unsafe
import safe Buggle
Предположим, что клиент C решил доверять пакету P и пакету base. Тогда C доверяет ли модулю M? Ну, M помечен как Trustworthy, поэтому мы не ограничиваем язык. Тем не менее, мы все еще должны проверить импорты M:
- Во-первых,
MимпортируетSystem.IO.Unsafe. Это небезопасный модуль, однакоMбыл скомпилирован сTrustworthy, поэтому авторPнесёт ответственность за этот импорт.Cдоверяет авторуP, поэтому этот импорт допустим. - Во-вторых,
Mбезопасно импортируетBuggle. В этом импорте авторPне несёт ответственности за безопасность, вместо этого попросив GHC проверить, доверяет лиCклиентуBuggle. Так ли это? -
Buggle, скомпилирован с-XSafe, поэтому код проверяется на безопасность, но опять же при условии, что все импортыBuggleдоверяютсяC. Мы должны рекурсивно проверять все импорты! - Buggle импортирует только
Prelude, который скомпилирован сTrustworthy.Preludeнаходится в пакетеbase, которомуCдоверяет, и (мы предположим), что все импортыPreludeдоверяются. Таким образом,CдоверяетPrelude, и поэтомуCтакже доверяет Buggle. (ХотяPreludeобычно импортируется неявно, он по-прежнему подчиняется тем же правилам, которые описаны здесь).
Обратите внимание, что C не нужно было доверять пакету Wuggle; достаточно проверки компилятором. C нужно доверять только пакетам, содержащим Trustworthy модули.
10.40.4.4. Требования к доверенным модулям
Авторы модулей, использующие расширение языка Trustworthy для модуля M должны гарантировать, что публичный API модуля M (символы, экспортируемые списком экспорта) не могут быть использованы небезопасным способом. Это означает, что экспортируемые символы должны соблюдать типевую безопасность и референциальную прозрачность.
10.40.4.5. Доверие к пакетам
Safe Haskell добавляет пакетам новое булево свойство — свойство доверия. В командной строке GHC доступны несколько новых параметров для указания свойства доверия пакетов:
-
-trust ⟨pkg⟩ -
Выявляет пакет ⟨pkg⟩, если он был скрыт, и считает его доверенным пакетом независимо от базы данных пакетов.
-
-distrust ⟨pkg⟩ -
Выявляет пакет ⟨pkg⟩, если он был скрыт, и считает его недоверенным пакетом независимо от базы данных пакетов.
-
-distrust-all-packages -
Считает все пакеты недоверенными, если они явно не определены как доверенные последующими параметрами командной строки.
Чтобы установить свойство доверия пакета в базе данных пакетов, обратитесь к Пакеты.
10.40.5. Выведение типов Safe Haskell
В случае, когда модуль компилируется без использования Safe, Trustworthy или Unsafe, GHC попытается самостоятельно определить, можно ли считать модуль безопасным. Это выведение типов никогда не будет отмечать модуль как доверенный, только как небезопасный или безопасный. GHC использует простой метод для определения этого для модуля M: если M был бы скомпилирован без ошибок с флагом Safe, то M будет помечен как безопасный. В противном случае он будет помечен как небезопасный.
Когда следует использовать выведение типов Safe Haskell, а когда следует использовать явный флаг Safe? В последнем случае следует использовать этот флаг, когда у вас жёсткое требование, чтобы модуль был безопасным. Это наиболее полезно для случаев использования Safe Haskell — для запуска недоверенного кода. Выведение типов Safe Haskell предназначено для обычных программистов Haskell. Пользователям, которые, вероятно, не заинтересованы в Safe Haskell.
Авторы библиотек Haskell могут выбрать. Большинство должны просто использовать выведение типов Safe. Предполагая, что вы избегаете любых небезопасных функций языка, ваши модули будут помечены как безопасные. У вывода типов и явного указания Safe есть следующие компромиссы:
- Выведенный тип — Это работает хорошо и не создаёт зависимости от типа Safe Haskell для каких-либо модулей в других пакетах. Это означает, что тип Safe Haskell ваших собственных модулей может измениться без предупреждения, если изменится зависимость. Один из способов справиться с этим — использовать флаги предупреждений Safe Haskell, которые будут предупреждать, если GHC выведет тип Safe Haskell, отличный от ожидаемого.
- Явный тип — Это даёт вашей библиотеке стабильный тип Safe Haskell, на котором другие могут полагаться. Однако это увеличит вероятность ошибок компиляции при изменении зависимостей вашего пакета.
10.40.6. Резюме флагов Safe Haskell
Вкратце, Safe Haskell состоит из следующих трех флагов языка:
-
Safe -
- Since
-
7.2.1
Ограничивает модуль безопасным языком. Все прямые импорты модуля должны быть доверенными, но сам модуль не обязан принадлежать к доверенному пакету, поскольку компилятор гарантирует его надёжность. Ключевое слово «safe» разрешено, но бессмысленно в операциях импорта, поскольку в любом случае каждый импорт должен быть безопасным.
- Модуль доверенный — Да
- Язык Haskell — Ограничен безопасным языком
- Импортируемые модули — Все должны быть безопасными импортами, все должны быть доверенными.
-
Trustworthy -
- С момента
-
7.2.1
Это подтверждает, что модуль надёжен, но гарантия предоставляется автором модуля. Клиент этого модуля затем указывает, что он доверяет автору модуля, указав, что он доверяет пакету, содержащему модуль.
Trustworthyне ограничивает модуль безопасным языком. Однако он ограничивает разрешение перекрывающихся экземпляров, позволяя только безопасные перекрывающиеся экземпляры. Он также позволяет использовать ключевое слово безопасного импорта.- Модуль надёжен — Да.
-
Модуль надёжен (
-fpackage-trustвключено) — Да, но только если пакет, в котором находится модуль, также является надёжным. - Язык Haskell — Без ограничений, за исключением допустимости только безопасных перекрывающихся экземпляров.
- Импортированные модули — Под контролем автора модуля, какие из них должны быть надёжными.
-
Unsafe -
- С момента
-
7.4.1
Отметьте модуль как небезопасный, чтобы его нельзя было импортировать кодом, скомпилированным с помощью
Safe. Также включите расширение безопасного импорта, чтобы модуль мог потребовать, чтобы зависимость была надёжной.- Модуль надёжен — Нет
- Язык Haskell — Без ограничений
- Импортированные модули — Под контролем автора модуля, какие из них должны быть надёжными.
Флаг для отключения проверок Safe Haskell:
-
-fno-safe-haskell -
Этот флаг можно включить для переопределения любого объявленного свойства безопасности модуля (Safe, Unsafe, Trustworthy), чтобы компиляция проходила так, как если бы ни один из этих флагов не был указан. Это особенно полезно при компиляции с использованием плагинов, что обычно приводит к тому, что скомпилированные модули отмечаются как небезопасные.
И один общий флаг:
-
-fpackage-trust -
При включении выполняется дополнительная проверка для надёжного модуля
M, требующая, чтобы пакет, в которомMнаходится, считался надёжным, чтобыMсчитался надёжным.
И пять флагов предупреждений:
-
-Wunsafe -
Выдать предупреждение, если компилируемый модуль считается небезопасным. Должен использоваться для проверки типа безопасности модулей при использовании безопасного вывода.
-
-Wsafe -
Выдать предупреждение, если компилируемый модуль считается безопасным. Должен использоваться для проверки типа безопасности модулей при использовании безопасного вывода. Если модуль явно отмечен как безопасный, то предупреждение не будет выдано.
-
-Wtrustworthy-safe -
Выдать предупреждение, если компилируемый модуль помечен как -XTrustworthy, но вместо этого он может быть помечен как -XSafe, более информативная граница. Может использоваться для обнаружения, когда границу Safe Haskell можно улучшить при обновлении зависимостей.
-
-Winferred-safe-imports -
- С момента
-
8.10.1
Модуль
Aниже аннотирован как явноSafe, но он импортирует модульSafe-Inferred.{-# LANGUAGE Safe #-} module A where import B (double) quad :: Int -> Int quad = double . double module B where double :: Int -> Int double n = n + nВыведенный статус изменчив: если к модулю
Bдобавится небезопасный импорт, это вызовет ошибку компиляцииA. При включении-Winferred-safe-imports, компилятор выведет предупреждение об этом. По умолчанию этот параметр выключен.
-
-Wmissing-safe-haskell-mode -
- С момента
-
8.10.1
Компилятор выведет предупреждение, если ни один из
Safe,TrustworthyилиUnsafeне указан. По умолчанию этот параметр выключен.
10.40.7. Безопасная компиляция
GHC включает ряд флагов, которые позволяют запускать произвольные процессы во время компиляции. Одним из таких примеров является флаг пользовательского препроцессора. Другой — возможность Template Haskell выполнять код Haskell во время компиляции, включая действия IO. Safe Haskell не решает эту проблему (хотя Template Haskell — запрещённая функция).
Из-за этого рекомендуется, что при компиляции ненадежного исходного кода, который не был проверен вручную, необходимо соблюдать следующие меры предосторожности:
- Компилировать в песочнице, например, в chroot или подобной контейнерной технологии. Или просто как пользователя с очень ограниченным доступом к системе.
- Компилировать ненадежный код с флагом
-XSafeна командной строке. Это гарантирует, что изменения в компилируемом исходном коде не смогут отключить использование безопасного языка, так как флаг командной строки имеет приоритет над прагмой уровня исходного кода. - Убедиться, что весь ненадежный код импортирован как безопасный импорт и что флаг
-fpackage-trust(см. флаг) используется с пакетами из ненадежных источников, которые помечены как ненадежные.
Более подробное обсуждение проблем, связанных с безопасностью компиляции, и возможных решений можно найти на википедии GHC.
Кроме того, использование аннотаций запрещено, так как это позволит обойти ограничения Safe Haskell. Подробнее см. #10826.
© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/8.10.2/docs/html/users_guide/safe_haskell.html