Модель безопасности
Aura исходит из того, что конфиги собирают из чужого кода (пакеты, шаблоны коллег, скопированные сниппеты), и строит защиту по построению, а не по договорённости.
Capability-модель
По умолчанию манифест не может ничего: ни читать файлы, ни переменные окружения. Права выдаются флагами запуска и не «протекают»:
| Флаг | Разрешает |
|---|---|
--allow-read=<dir> | read_file() внутри каталога; пути канонизируются, .. не сбегает (E0311). Относительный путь читается от папки самого манифеста (D37) |
--allow-env[=A,B] | env() для перечисленных переменных (без списка — все) |
--hermetic | I/O нет вовсе: env() и read_file() — E0505 в любом модуле; несовместим с --allow-* |
--allow-imports-io | распространить права корня на импортированные модули |
Вызов без прав — E0310 с подсказкой, какой флаг добавить.
Герметичный режим
--hermetic — та же шкала с другого конца: вместо выдачи прав он объявляет, что
прав нет вовсе. env() и read_file() становятся E0505 где угодно — и в корне,
и в импорте, — а сам флаг несовместим с любым --allow-*.
Отличие от простого «не выдавать права» — в том, когда узнаёшь. Запрещённый
вызов — это E0310 во время выполнения, на той ветке, которая исполнилась;
E0505 — ошибка анализа, поэтому aura check --hermetic доказывает свойство для
всего манифеста, ничего не вычисляя. Это и делает его пригодным для гейта в CI:
$ aura check --hermetic manifests/*.aura
CUE, KCL и Starlark герметичны по построению. В Aura это режим — так один репозиторий может держать и манифесты, которым нужно читать окружение, и те, которым нельзя, а CI может требовать строгий вид по каталогам.
Ограниченное вычисление
Герметичность доказывает, что манифест ничего не может взять. Но не доказывает,
что он закончится: три вложенных range(1000).map умещаются в 95 байт, проходят
check --hermetic и работали бы больше минуты. Поэтому у каждого вычисления есть
бюджет — у CLI, eval_file, песочницы и Script одинаково:
| Бюджет | Исчерпан | По умолчанию | Флаг |
|---|---|---|---|
| шаги — вычисленные выражения | E0325 | 10 млн | --max-steps=N |
| байты — каждое созданное значение и кадр | E0326 | 256 МиБ | --max-memory=MB |
Оба считаются, а не замеряются по времени, поэтому один и тот же манифест
останавливается в одной и той же точке на любой машине. Значения по умолчанию
примерно в тридцать тысяч раз больше, чем тратит самый тяжёлый пример в
репозитории; --stats печатает расход, чтобы бюджет выбирался по замеру.
У глубины свой предел, потому что шаги её не ограничивают: функция, вызывающая
себя несколько сотен раз, тратит мало шагов и очень много стека. Одно выражение
вкладывается не глубже 256 уровней - длинная цепочка вроде a + b + c + ...
засчитывает свою длину, - а вычисление во время работы вкладывается не глубже 256
уровней, причём рекурсивный вызов засчитывает выражения, через которые проходит.
За любым из пределов - ошибка, E0208 или E0399, а не переполнение стека (D42).
Как и бюджет, предел - это счёт, поэтому манифест останавливается в одной и той
же точке везде. Release-сборке на него хватает 512 КиБ стека, так что хост может
вызывать Script с любого потока, где стека не меньше, - у вторичного потока iOS
ровно 512 КиБ.
У значений тот же предел (D43). Цикл может построить глубокое значение без
всякой собственной вложенности - reduce, заворачивающий аккумулятор, добавляет
уровень на каждом шаге, - а любой обход значения, от вывода до освобождения
памяти, рекурсивен по уровням. Поэтому список или объект глубже 256 - это E0328
там, где его строят, а значение от хоста такой глубины отвергается на границе.
Разобранные JSON и YAML и так останавливаются на 128.
Изоляция импортов
Права принадлежат корневому манифесту. Импортированный модуль:
- не может вызывать
env()/read_file()— даже если у корня права есть; - не получает права через свои экспортированные функции: тело
pub defвыполняется с capability модуля-происхождения, а не вызывающего; - статически помечается предупреждением
W0512, если содержит эффектные вызовы.
$ aura eval main.aura --allow-read=.
[E0310] Error: imported module has no capability to call read_file()
╭─[ evil_dependency.aura:2:7 ]
Живой пример — examples/security_demo/.
Функции хоста и перезагрузка
Приложение, встраивающее Aura, может дать скрипту свои функции (D35) и перезагружать его на ходу (D36). Оба механизма подчинены той же модели:
- Функция хоста - это разрешение, которое приложение выдаёт по имени, по одной.
pure- обещание хоста, что функция детерминирована, - оставляет скрипт герметичным;effect- разрешение, как--allow-env. - Они доступны только корневому модулю. Импортированный пакет не может их
вызвать, если приложение не открыло ему эту функцию по имени (
E0310). - Каждое использование проверяется при загрузке, до любого вычисления:
непредоставленная функция -
E0007. Функция хоста не может вызвать скрипт обратно (E0010). - Перезагрузка - это полная загрузка с разрешениями первой и её
aura.lock, замороженным: правка скрипта не расширит его права, не подтянет незакреплённый пакет и не уберёт экспорт, к которому привязалось приложение (E0011). Недописанный файл не загружается, пока его не оставят в покое на период тишины.
Supply chain
- Версия пакета обязательна в самом импорте:
import github/acme/pkg@v1.2 as p. aura.lockфиксирует точную версию и хеш потока токенов модуля; подмена —E0402. Хеш по токенам, а не по байтам, означает, что переформатирование или правка комментария проверку не срабатывают, а любое изменение, способное повлиять на поведение, — срабатывает. Lock, записанный старой версией, содержит байтовыйsha256-; он по-прежнему проверяется и обновляется на первом же запуске без--frozen.--frozen(режим CI) запрещает резолв мимо лока (E0403).- Сеть существует только в
aura add; вычисление всегда оффлайн.
Детерминизм
now()не существует (E0533) — время передаёт хост черезenv.- Порядок ключей вывода = порядок объявления; два прогона побайтно идентичны.
--dry-runвыполняет всё вычисление, но не пишет ни JSON, ни лок, и отчитывается о каждом чтении:[dry-run] read: ./Cargo.toml.