Стандарт WASI: запуск WebAssembly за пределами веба

27 марта мы в Mozilla объявили о начале стандартизации WASI, системного интерфейса WebAssembly (WebAssembly system interface).

Зачем: разработчики начали применять WebAssembly за пределам браузера, потому что WASM обеспечивает быстрый, масштабируемый, безопасный способ запуска одинакового кода на всех машинах. Но у нас пока нет прочного фундамента для такой разработки. Вне браузера нужен некий способ общения с системой, то есть системный интерфейс. А у платформы WebAssembly его пока нет.

Что: WebAssembly — это ассемблер для концептуальной, а не физической машины. Он работает на различных архитектурах, поэтому и системный интерфейс нужен для концептуальной ОС, чтобы работать на разных операционных системах.

Вот что такое WASI: это системный интерфейс для платформы WebAssembly.

Мы стремимся создать системный интерфейс, который станет настоящим компаньоном для WebAssembly с максимальной переносимостью и безопасностью.

Кто: в рамках группы разработки WebAssembly мы организовали подгруппу, которая займётся стандартизацией WASI. Мы уже собрали заинтересованных партнёров и ищем новых.

Вот некоторые причины, по которым мы, наши партнёры и сторонники считаем это важным:

Шон Уайт, директор Mozilla по R&D:
«WebAssembly уже меняет способы доставки людям новых видов привлекательного контента, Он помогает разработчикам и создателям контента. До сих пор всё работало через браузеры, но с WASI преимущества WebAssembly получат больше пользователей и больше устройств в разных местах».
Тайлер МакМаллен, технический директор Fastly:
«Мы рассматриваем WebAssembly как платформу для быстрого и безопасного выполнения кода на граничном облаке (edge cloud). Несмотря на разную среду (edge и браузеры), благодаря WASI не придётся портировать код на каждую платформу».
Майлз Боринс, технический директор руководящего комитета Node:
«WebAssembly может решить одну из самых больших проблем Node: как добиться близкой к нативной скорости и повторно использовать код, написанный на других языках, таких как C и C++, сохраняя портативность и безопасность. Стандартизация WASI является первым шагом к этому».
Лори Восс, соучредитель npm:
«npm чрезвычайно взволнован потенциальными возможностями WebAssembly для экосистемы npm, поскольку значительно упрощается получение нативного кода для запуска в серверных приложениях JavaScript. Мы с нетерпением ожидаем результатов этого процесса».
Так то это большое событие!

В настоящее время есть три реализации WASI:

Демонстрация WASI в действии:


Дальше мы расскажем о предложении Mozilla, как должен работать этот системный интерфейс.

Что такое системный интерфейс?
Многие говорят, что языки вроде C дают прямой доступ к системным ресурсам. Но это не совсем так. В большинстве систем эти языки не имеют прямого доступа к таким вещам, как открытие или создание файлов. Почему нет?

Потому что эти системные ресурсы — файлы, память и сетевые подключения — слишком важны для стабильности и безопасности.

Если одна программа случайно испортит ресурсы другой, то может вызвать сбой. Хуже того, если программа (или пользователь) специально вторгается в чужие ресурсы, то может украсть конфиденциальные данные.

Поэтому нужен способ контролировать, какие программы и пользователи могут получить доступ к ресурсам. Системные разработчики довольно давно придумали способ обеспечить такой контроль: кольца защиты.

С кольцами защиты ОС по сути устанавливает защитный барьер вокруг системных ресурсов. Это ядро. Только оно может выполнять такие операции, как создание файла, открытие файла или открытие сетевого подключения.

Программы пользователя работают вне ядра в так называемом пользовательском пространстве. Если программа хочет открыть файл, она должна попросить ядро.

Именно здесь возникает концепция системного вызова. Когда программе нужно попросить ядро о какой-то операции, она отправляет системный вызов. Ядро проверяет обратившегося пользователя и видит, есть ли у него права на доступ к этому файлу.

На большинстве устройств единственный способ доступа к ресурсам системы — через системные вызовы.

Операционная система предоставляет доступ к системным вызовам. Но если у каждой ОС собственные системные вызовы, разве для них не нужно писать разные версии кода? К счастью, нет. Проблема решается с помощью абстракции.

В большинстве языков есть стандартная библиотека. При кодировании программисту не нужно знать, для какой системы он пишет. Он просто использует интерфейс. Затем, при компиляции, ваша цепочка инструментов выбирает, какую реализацию интерфейса использовать для какой системы. Эта реализация использует функции из API операционной системы, поэтому она специфична для неё.

Вот где появляется понятие системного интерфейса. Например, если компилировать printf для машины Windows, он будет использовать Windows API. Если компилировать для Mac или Linux, он использует POSIX.

Однако это создаёт проблему для WebAssembly. Здесь мы не знаем, для какой ОС оптимизировать программу даже во время компиляции. Таким образом, вы не можете использовать системный интерфейс какой-то одной ОС внутри реализации стандартной библиотеки на WebAssembly.

Я уже говорила, что WebAssembly — это ассемблер для концептуальной машины, а не реальная машина. Точно так же WebAssembly нуждается в системном интерфейсе для концептуальной, а не реальной ОС.

Но уже есть среды выполнения, которые могут запускать WebAssembly вне браузера, даже без этого системного интерфейса. Как они это делают? Давайте посмотрим.

Как WebAssembly сейчас работает вне браузера?
Первым инструментом для генерации кода WebAssembly был Emscripten. Он эмулирует в вебе определённый системный интерфейс ОС — POSIX. Это означает, что программист может использовать функции из стандартной библиотеки C (libc).

Для этого Emscripten пользуется собственной реализацией libc. Она разделена на две части: первая скомпилирована в модуль WebAssembly, а другая реализована в коде «JS-клея». Этот JS-клей отправляет вызовы браузеру, который разговаривает с ОС.

Основная часть раннего кода WebAssembly скомпилирована с Emscripten. Поэтому, когда люди начали хотеть запускать WebAssembly без браузера, то начали запускать код Emscripten.

Так что в этих рантаймах следовало создать собственные реализации для всех функций, которые были в коде JS-клея.

Но тут есть проблема. Интерфейс, предоставляемый кодом клея JS, не был разработан как стандартный или даже общедоступный интерфейс. Например, для вызова вроде read в нормальном API код JS-клея использует вызов _system3(which, varargs).

Первый параметр which — целое число, которое всегда совпадает с числом в имени (в нашем случае 3).

Второй параметр varargs перечисляет аргументы. Он называется varargs, потому что у нас может быть разное число аргументов. Но WebAssembly не позволяет передать функции переменное количество аргументов. Поэтому они передаются через линейную память, что нетипобезопасно и медленнее, чем через регистры.

Для Emscripten в браузере это нормально. Но теперь среды выполнения рассматривают это как стандарт де-факто, реализуя собственные версии JS-клея. Они эмулируют внутренние детали эмуляционного слоя POSIX.

Это означает, что они заново реализуют код (например, передают аргументы как значения кучи), что имело смысл с учётом ограничений Emscripten, но в этих средах выполнения нет таких ограничений.

Если мы строим экосистему WebAssembly на десятилетия вперёд, для неё нужен прочный фундамент, а не костыли. Это означает, что наш фактический стандарт не может быть эмуляцией эмуляции.

Но какие принципы применить а таком случае?

Какие принципы должен соблюдать системный интерфейс WebAssembly?
Два основополагающих принципа WebAssembly:

  • портируемость
  • безопасность

Мы выходим за пределы браузера, но сохраняем эти ключевые принципы.

Однако подход POSIX и система управления доступом Unix не даёт нам искомый результат. Давайте посмотрим, в чём проблема.

Портируемость
POSIX обеспечивает переносимость исходного кода. Вы можете скомпилировать один и тот же исходный код с разными версиями libc для разных компьютеров.

Но WebAssembly должен выйти за эти рамки. Нам нужно компилировать однажды для запуска на целой куче разных систем. Нам нужны портируемые бинарники.

Это упрощает распространение кода.

Например, если нативные модули Node написаны в WebAssembly, то пользователям не нужно запускать node-gyp при установке приложений с нативными модулями, а разработчикам не нужно настраивать и распространять десятки бинарных файлов.

Безопасность
Когда код просит операционную систему сделать ввод или вывод, ОС должна оценить безопасность этой операции, обычно с помощью системы управления доступом, основанной на владении и группах.

Например, программа просит открыть файл. У пользователя есть определённый набор файлов, к которым он имеет доступ.

Когда пользователь запускает программу, программа запускается от имени этого пользователя. Если пользователь имеет доступ к файлу — либо он его владелец, либо входит в группу, имеющую доступ к файлу, — то у программы такой же доступ.

Это защищает пользователей друг от друга, что имело смысл в старые времена, когда на одном компьютере работало много людей, а администраторы контролировали программное обеспечение. Тогда главной угрозой были другие пользователи, заглядывающие в ваши файлы.

Всё изменилось. В настоящее время системы, как правило, являются однопользовательскими, но используют сторонний код неизвестной надёжности. Сейчас основная угроза исходит от кода, который вы сами запускаете.

Например, для библиотеки в вашем приложении завёлся новый мейнтейнер (как часто бывает в open source). Он может быть искренним активистом… или злоумышленником. И если у него появляется доступ к вашей системе — например, возможность открыть любой файл и отправить его по сети — тогда этот код может нанести большой ущерб.


Подозрительное приложение: Я работаю для пользователя Боб. Можно мне открыть его кошелёк Bitcoin?
Ядро: Для Боба? Конечно!
Подозрительное приложение: Отлично! А что насчёт сетевого подключения?

Вот почему опасно использование сторонних библиотек. В WebAssembly безопасность обеспечивается иначе — через песочницу. Здесь код не может напрямую разговаривать с ОС. Но как тогда обращаться к системным ресурсам? Хост (браузер или среда выполнения wasm) помещает в песочницу функции, которые код может использовать.

Это означает, что хост на программной основе ограничивает функциональность программы, не позволяя просто действовать от имени юзера, вызывая любые системные вызовы с полными правами пользователя.

Наличие песочницы само по себе не делает систему безопасной — хост всё ещё может передать в песочницу полную функциональность, и в этом случае она не обеспечивает никакой защиты. Но песочница даёт хотя бы теоретическую возможность хостам построить более безопасную систему.


WA: Пожалуйста, вот тебе несколько безопасных игрушек для взаимодействия с ОС (safe_write, safe_read).
Подозрительное приложение: Ох блин… а где ж мой доступ к сети?

В любом системном интерфейсе нужно придерживаться этих двух принципов. Переносимость облегчает разработку и распространение программного обеспечения, а инструменты для защиты хоста и пользователей абсолютно необходимы.

Как должен выглядеть такой системный интерфейс?
Учитывая эти два ключевых принципа, каким должен быть системный интерфейс WebAssembly?

Это мы выясним в процессе стандартизации. Впрочем, у нас есть предложение для начала:

  • Создание модульного набора стандартных интерфейсов
  • Начнём со стандартизации основного модуля wasi-core

Что будет в wasi-core? Это основы, необходимые всем программам. Модуль покроет большую часть функциональности POSIX, включая файлы, сетевые подключения, часы и случайные числа.

Многое из базовой функциональности потребует очень похожего подхода. Например, предусмотрен файл-ориентированный подход POSIX с системными вызовами open, close, read и write, а всё остальное — дополнения сверху.

Но wasi-core не охватит всю функциональность POSIX. Например, концепция процесса чётко не укладывается в WebAssembly. Кроме того, совершенно очевидно, что каждый движок WebAssembly должен поддерживать операции процесса, такие как fork. Но мы также хотим сделать возможным стандартизацию fork.

Языки вроде Rust будут использовать wasi-core непосредственно в своих стандартных библиотеках. Например, open из Rust реализуется при компиляции в WebAssembly вызовом __wasi_path_open.

Для C и C++ мы создали wasi-sysroot, который реализует libc в терминах функций wasi-core.

Мы ожидаем, что компиляторы, такие как Clang, смогут взаимодействовать с WASI API, а полные цепочки инструментов, такие как компилятор Rust и Emscripten, будут использовать WASI как часть своих системных реализаций.

Как пользовательский код вызывает эти функции WASI?

Рантайм, в котором выполняется код, передаёт функции wasi-core, помещая объект в песочницу.

Это обеспечивает переносимость, потому что у каждого хоста может быть собственная реализация wasi-core специально для его платформы: от рантаймов WebAssembly, таких как Mozilla Wasmtime и Fastly Lucet, до Node или даже браузера.

Это также обеспечивает надёжную изоляцию, потому что хост на программной основе выбирает, какие функции wasi-core передать в песочницу, то есть какие системные вызовы ей разрешить. Это безопасность.

WASI позволяет усилить и расширить безопасность, привнося в систему концепцию защиты на основе полномочий.

Обычно, если коду необходимо открыть файл, он вызывает open с именем пути в строке. Затем ОС проверяет, есть ли у кода право на такое действие (исходя из прав пользователя, запустившего программу).

В случае с WASI при вызове функции для доступа к файлу вы должны передать файловый дескриптор, к которому прикреплены разрешения для самого файла или для каталога, содержащего файл.

Таким образом, у вас не может быть кода, который случайно попросит открыть /etc/passwd. Вместо этого код может работать только со своими каталогами.

Это позволяет безопасно разрешить изолированному коду различные системные вызовы, поскольку возможности этих системных вызовов ограничены.

И так в каждом модуле. По умолчанию, модуль не имеет доступа к файловым дескрипторам. Но если у код в одном модуле есть файловый дескриптор, он может передать его функциям, вызываемым в других модулях. Или создать более ограниченные версии файлового дескриптора для передачи другим функциям.

Таким образом, среда выполнения передаёт файловые дескрипторы, которые приложение может использовать в коде верхнего уровня, а затем файловые дескрипторы распространяются по остальной части системы по мере необходимости.

Это приближает WebAssembly к принципу наименьших привилегий, где модуль получает доступ только к минимальному набору ресурсов, необходимому для выполнения своей работы.

Данная концепция исходят из защиты на основе полномочий, как в системах CloudABI и Capsicum. Одна из проблем этих систем в трудной переносимости кода. Но мы считаем, что эту проблему можно решить.

Если код уже использует openat с относительными путями файлов, компиляция кода будет просто работать.

Если код использует open и миграция в стиле openat слишком резкая, WASI предоставит инкрементное решение. С помощью libpreopen вы создаёте список путей к файлам, к которым приложение имеет законный доступ. Затем используете open, но только с этими путями.

Что дальше?
Мы считаем, что wasi-core — хорошее начало. Он сохраняет портируемость и безопасность WebAssembly, обеспечивая прочную основу для экосистемы.

Но после полной стандартизации wasi-core нужно решить другие вопросы, в том числе:

  • асинхронный ввод-вывод
  • наблюдение за файлами
  • блокировка файлов

Это только начало, так что если у вас есть идеи, подключайтесь к работе!

Оставить комментарий