Всё, что вы хотели знать о Singularity, но боялись спросить

:

Мне бы хотелось написать что-нибудь про Microsoft Singularity. Это очень клёвая штука, и в IT сегодня все говорят про это. Вот обзор Singularity для тех, кто не хочет читать официальные публикации.


Singularity — это исследовательский проект по разработке операционной системы. Это команда умных людей, которым сказали: «Как бы вы сделали операционную систему, изначально ориентированную на надёжность?».

Люди на популярных форумах вроде Slashdot или OSNews уже годами мечтают о том, чтобы Microsoft отказалась от Windows и начала всё с чистого листа — чтобы побороть проблемы ненадёжности или вредоносного кода (malware). Обычно мечтания всех этих людей крутятся вокруг идеи переноса Windows на какой-нибудь UNIX — но это говенная идея, это не решило бы основных проблем. Если вы хотите решить проблемы, вызванные фундаментальными особенностями дизайна, вам следует начать с дизайна. Что и делается в Singularity.

Надёжность — это довольно обширный вопрос. Как минимум, это означает, что приложения не падают и что они безопасны. Но несмотря на то, что разработка Singularity затрагивает множество областей, у разработчиков нет задачи сделать полноценную ОС — к примеру, они не занимаются разработками GUI.


Собственно, это то, о чём я буду говорить здесь.

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

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

Singularity — высокопроизводительна. Вообще-то, производительность не была целью проекта, но разработчики оказались достаточно умны для осознания того, что без этого их разработка «улетит в облака» и станет совершенно некоммерциализируемой. Это косвенно указывает на то, что Microsoft планирует когда-нибудь использовать эту разработку в реальных продуктах, так что ей необходимо быть быстрой (об этом немного попозже).

А еще Singularity — это микроядро. Давайте сейчас сфокусируемся на том, что это такое. Мы поговорим об остальных вещах позже. Прокрутите вниз, если вы уже в курсе всего этого — в таком случае будем считать, что вы совершенно точно знаете, что такое микроядро и почему оно считается медленным.


Исторически сложились два способа спроектировать операционную систему, два способа сделать одно и то же, используя разные соотношения затрат и результатов: микроядро или монолитное ядро. Заметьте, мы сейчас говорим о низкоуровневом дизайне… это не влияет на то, используете ли вы taskbar или dock в своём UI.

В микроядерном дизайне подсистемы вроде файловой системы или сетевых драйверов работают как более-менее обычные программы вне самого ядра (которое отличается от других программ тем, что работает в специальном режиме процессора). Ядро отвечает лишь за создание процессов/потоков, пересылку сообщений между ними и за некоторые другие мелкие вещи, типа распределения ресурсов CPU. Истинных микроядер на сегодняшний день практически нигде не встретишь. Скорее всего, вы использовали его, даже не подозревая об этом. К примеру, QNX — операционная система, разработанная для встроенных приложений, вроде роутеров Cisco. QNX — это чистое микроядро.

Хозяйке на заметку


Вот краткий обзор виртуальной памяти. Когда ваш код читает что-то из памяти, CPU внутри преобразовывает ваш адрес из виртуального адреса в физический адрес, который он может скормить контроллерам памяти. На 32-х битном CPU они оба являются 32-х битными указателями, и вы, скорее всего, никогда не увидите непосредственный физический адрес, если только вы не ядерный разработчик. Это преобразование делается компонентом процессора, который называется MMU (memory management unit) — он также реализует контроль доступа. Память разбивается на «страницы» по 4 килобайта (в чипах Intel/AMD) и для каждой страницы заводится своё отображение (mapping). У страницы отображения есть биты доступа — read/write/execute — прямо как у файлов в UNIX.

Такое отображение памяти является основой безопасности во всех операционных системах. Оно не даёт глючащей программе гадить в память других программ, и — поскольку лишь ядро может обновлять таблицы страниц памяти, плюс весь доступ к железу происходит через ядро — такая программа, работающая в пользовательском режиме процессора, ничего «интересного» сделать не сможет, пока ядро не позволяет это делать. А поскольку MMU не позволяет вам читать память ядра, вы не можете получить доступ к ней. Это также означает, что мы можем использовать своп-файлы для использования диска, как чипа RAM — просто выгрузите отображение части адресного пространства процесса, поймайте ошибку чтения из него — и загрузите отображение вновь.

Виртуальная память — отличная штука, одно из самых больших улучшений надежности компьютеров за последние 13 лет. Windows 3.1 не использовала её, в отличие о Windows 95 — и вот почему так много людей обновили свою OС. Преимущества микроядра здесь очевидны — глючные компоненты ядра не могут отправить компьютер в BSOD aka «blue screen of death» — как они делают это сегодня. Если ваша файловая система обрушилась — просто перезапустите её!

В монолитном дизайне файловые системы, драйверы и даже веб-серверы загружаются прямо в ядро и работают в привилегированном режиме процессора. Ядро всё еще предоставляет систему обмена сообщениями (message-passing) для процессов пользовательского режима — но это нигде особо не используется. Сегодня монолитной является любая популярная серверная или десктопная ОС — Windows, Linux и MacOS. Заметьте, что Linux всегда был монолитным, Windows NT изначально была микроядерной, а MacOS — будучи основанной на Mach — теоретически микроядерна. Впрочем, я не знаю никого, кто бы в это верил.

Вероятно, трудно сказать, действительно ли какая-либо операционная система микроядерна или монолитна, потому что это не бинарная логика «да/нет» — к примеру, в Linux графическая подсистема работает в отдельном процессе (X Server), тогда как в Windows когда-то было так — но не сегодня. Так или иначе, никто не спорит, что Linux — это не микроядро. Хороший критерий оценки это то, работает ли файловая система в режиме ядра или нет — может, графические системы и находятся в серой области, но не файловые системы.

В любом случае, то, что в Singularity применяется микроядерный дизайн, выглядит довольно странно, потому что исторически в академической среде побеждало микроядро, а вот на рынке всегда побеждало монолитное ядро — в основном из-за проблем с производительностью. Эти споры разгорались в 80-х — вот здесь вы можете прочитать знаменитый спор Торвальдса и Таненбаума. Итак, на первый взгляд может показаться, что Singularity это очередное академическая разработка теоретически чистой, но практически неюзабельной вещи… но это не так.


Микроядра типично медленнее, чем монолитные ядра — из-за накладных расходов на переключения между режимами процессора (user-mode в kernel-mode и обратно). Кроме того, существуют еще и расходы на переключение процессора между двумя процессами пользовательского режима (переключение контекста).

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

Причина, по которой эти переключения отнимают драгоценное время заключается в том, что CPU приходится делать необычную для него работу, а поскольку большую часть времени процессор проводит не делая эту работу — она не очень хорошо оптимизирована. Это изменилось в последних поколениях чипов x86, но в целом, всё остаётся так же.

К примеру, делая syscall чтобы заставить ядро сделать что-то, вы используете специальную инструкцию процессора. Обычно это «int 80» на Linux, но сегодня вы можете использовать опкод «sysenter» на ядрах и процессорах, поддерживающих его (практически все умеют). При этом управление переходит в ядро. Это довольно быстро на современных компьютерах, но это не всегда было так — к примеру, ранние версии Windows применяли некорректную инструкцию процессора, так как разработчики обнаружили, что выброс исключения CPU оказался более быстрым способом попасть в режим ядра, чем использование прерывания («официальный» способ). В Intel исправили это :)

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

Небыстрая она из-за того, что это, опять же, необычная операция (для этого нужно использовать специальные регистры в x86 чипах), но в основном из-за того что это требует сброса так называемых «translation lookaside буферов» (TLB). Эти буферы хранят результат запросов MMU. Ведь даже если MMU и являются специализированным для своей задачи железом, его использование не бесплатно — а трансляцию адресов памяти надо делать каждый раз при обращении кода к памяти (что происходит постоянно) — и здесь кэширование просто необходимо.

Из-за этого трудно измерить, насколько дорого обходится переключение контекста. Мы знаем, что это чего-то там стоит — из-за фундаментальных особенностей дизайна CPU. Но реальная стоимость размазывается по коду запущенного процесса. Сразу после переключения контекста ваш компьютер работает немного медленнее и набирает ход по мере заполнения TLB.

Итак, мы имеем два конфликтующих приоритета. С одной стороны, использование виртуальной памяти для разделения адресных пространств может улучшить надёжность отделением программ друг от друга (что хорошо), но, с другой стороны, это обходится нам сложноизмеримой потерей производительности — что плохо. Еще хуже то, что хоть процессоры и становятся быстрее время от времени — они становятся быстрее в исполнении кода, но не в манипуляциях с адресным пространством, так что мы не можем положиться на помощь закона Мура в этот раз.


Микро-ядра основаны на идее пересылки сообщений между процессами в разных адресных пространствах. Так, чтобы прочитать файл, вам нужно послать сообщение из вашей программы в сервер файловой системы. Вы формируете сообщение в вашей памяти (это быстро), делаете syscall «послать сообщение» (уже не так быстро), потом ядро копирует сообщение в своё собственное адресное пространство (довольно медленно), переключает контекст на сервер файловой системы (медленно), и потом копирует сообщение в память сервера файловой системы перед тем, как покинуть режим ядра.

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

Совсем иначе это выглядит в монолитном дизайне: вы формируете ваш запрос (это быстро), делаете syscall (не так быстро), ждёте, пока файловая система не получит ваши данные, потом ядро копирует данные прямо в ваше адресное пространство (или это сделает железо, если вы используете DMA), возвращая управление в пользовательский режим… wow, это проще и быстрее! Недостаток здесь в том, что если файловая система заглючит, то вся ОС порушится в BSOD и вы потеряете всё.

Около 80% крэшей в Windows вызвано кривыми драйверами. Итак, если бы мы смогли предотвратить крэши системы из-за драйверов таким же образом, как мы предотвратили крэши из-за глючных программ — мы предотвратили бы 80% всех BSOD'ов в мире! Это довольно круто! Это также означает, что с драйверами могут работать механизмы безопасности. Если вы сегодня установите стороннюю файловую систему, кто знает, что вы на самом деле получите? Пока вы не просмотрите и не скомпилируете код сами — вам приходится доверять тому, кто дал вам его. Даже если с этим всё в порядке, баг в новом драйвере может открыть дыру для руткита, нагибая всю систему безопасности :(

Наверное, это не удивительно, что академики предпочли медленное, но надёжное решение — а разработчики десктопных ОС предпочли быстрое, но нестабильное. Но не надо думать, что они не пытались сделать иначе! Windows NT разрабатывалась как чисто микроядерное решение, но даже с супер-оптимизированным межпроцессовым взаимодействием они однажды сдались и «переехали» в ядро вместе с GUI — за что получили массу критики, но это сделало Windows отзывчивой, а юзеров счастливыми.


Singularity умудряется «и сесть и съесть» — получить все преимущества микроядра и даже более высокую производительность, чем монолитное ядро. Круто!
Singularity впечатляет тем, что, обладая чистым микроядерным дизайном, на ~30% быстрее традиционного подхода и на 10% быстрее монолитного (в бенчмарке тяжёлого файлового I/O). Как же она это делает?

Трюк очень прост — они просто отказались от аппаратной защиты памяти, полностью. В Singularity всё работает в режиме ядра и всё работает в едином адресном пространстве. Иными словами, MMU ничего не делает. И тут нет «процессов» в традиционном понимании.

Разумеется, если бы это было единственной вещью, которую они сделали, это не было бы так интересно. Восемьдесят процентов крэшей Windows вызвано глюкавыми драйверами, а не ошибками в ядре (подозреваю, что остальные 20% вызваны сбоями в железе). Бессчётные уязвимости, позволяющие эскалировать привилегии вызваны глюками в драйверах. Каждая такая уязвимость — подарок «плохим ребятам». Защита от программных ошибок — вот, что делает микроядра полезными.

Программы в Singularity изолированы друг от друга — но изоляция делается целиком и полностью программно, с помощью теории типов, а не кремния. Это возможно благодаря тому, что в Singularity программы написаны на потомке C#, названном Sing# (теоретически, вы можете использовать любой язык для .NET — Singularity использует совсем немного фич, добавленных в C#, так что любой другой язык потребует лишь этих немногих изменений).

Вы наверняка уже знаете, что в большинстве современных языков — вроде Java или C# — у вас нет прямого доступа к памяти. Грубо говоря, нельзя написать подобный C-код на Java:

*((char *)0x1234) = ‘X’; // записать байт по адресу 1234

И дело не в том, что синтаксиса для этого нет. Просто компиляторы Java/C# производят байт-код, который может быть проверен математическими методами, чтобы удостовериться, что код не делает этого. И, будучи проверенными, эти опкоды JVM/MSIL транслируются в «настоящий код», который скармливается процессору, и мы уверены в том, что передав в этот код ссылку на объект, код не будет читать/писать в него. И это не только обеспечивает надёжность — мы можем построить целую систему безопасности поверх этого!


Окей, это было в теории. А на практике это не совсем так. Точно так же, как теоретически изолированные программы на C могут нарушить изоляцию, используя уязвимости в ядре — так же и программы на Java/.NET могут использовать уязвимости в среде исполнения или нативных библиотеках классов (написанных на C++) и таким образом обойти систему безопасности.

А еще хуже то, что в некоторых случаях, механизмы вроде reflection (интроспекция) могут быть использованы для доступа к объектам тогда, когда такое поведение не ожидается.

Обе этих проблемы служили причиной для эксплойтов в JVM-апплетах в прошлом. Учитывая то, насколько огромны современные JRE — это совсем не удивительно.


Подход Singularity снова очень прост — вся система написана целиком на C#, лишь малая часть написана на C++ и ассемблере. Это автоматически делает ядро устойчивым к распространенным видам атак. Но Microsoft на этом не останавливается — они работают над исследованиями, которые помогут доказать безопасность и того оставшегося небольшого объема небезопасного кода.

Основной строительный блок Singularity — это SIP, software isolated process или «программно изолированный процесс». Singularity придерживается идеи процесса, как чего-то, что обладает собственными ресурсами, своим адресным пространством и независимо шедулится на исполнение — но это всё обеспечивается исключительно математикой.

SIP обладает собственной кучей (heap), собственным сборщиком мусора (вы можете выбрать из нескольких разных предоставляемых системой GC, принимая во внимание тот факт, что ни один GC не подходит для всех ситуаций) и своими собственными страницами памяти. Так что SIP более изолирован, чем в некоторых других решениях (типа KaffeOS), в которых весь код загружается в единую среду исполнения и объекты могут быть разделены между разными программами. Недостаток в том, что вы не можете с легкостью обмениваться объектами между двумя SIP. А объективное преимущество в том, что SIP могут сильно отличаться друг от друга — используя не только разные сборщики мусора, но и разные языковые среды исполнения и разные модели данных. Это также означает, что они могут быть быстро уничтожены, без прогонки GC по всему хипу, что может быть невероятно медленно.

Мы больше не используем различные режимы CPU — и теперь неясно, как определить, что является «ядром». В Singularity «ядро» это программный компонент, который связывает SIP'ы вместе, управляет памятью, загружает новые SIP'ы и делает некоторые другие задачи. Логически, оно также включает в себя доверенные/небезопасные части системы, написанные на C++ или ассемблере. Некоторые из них привязываются к SIP (сборщики мусора), но, поскольку они считаются безопасными, они могут считаться частью ядра.

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

SIP'ы общаются через так называемые «каналы». Каналы — это основанные на сообщениях пайпы, вроде UNIX'овых сокетов, но строго типизированных и быстрых. Канал это, по сути, математическая абстракция — пересылка сообщения через такой пайп не вызывает копирования чего-либо или использования всяких железячных трюков. Пересылка сводится к обновлению пары указателей в памяти. И поэтому пересылка даже больших объемов данных между SIP'ами (к примеру, между сетевым драйвером и веб-сервером) очень быстра.


Может показаться диким то, что обращение к ядру сводится к простому вызову функции. Это ведь невозможно? Что мешает SIP'у просто вызвать функции для управления жестким диском?

Ответ в том, что SIP поставляется (скажем, на CD), как набор MSIL-инструкций. Эти файлы не компилируются при исполнении (just-in-time, JIT) так же, как в обычной среде Java или .NET. Вместо этого, при установке программы она компилируется и статически проверяется различными анализаторами. Установка программ это привилегированная операция в Singularity — она производится самим ядром. Лишь программы установленные ядром допускаются к исполнению.

Поскольку вы не можете определить в безопасном MSIL такие специфичные для CPU инструкции, как «записать в этот I/O порт» или «вызвать это прерывание», единственный способ сделать это — слинковаться с какой-нибудь доверенной нативной библиотекой, которая сделает это для вас. Поскольку ядро управляет установкой программ, оно может удостовериться, что лишь определенные программы связаны с такими библиотеками, и что связаны они лишь определенным образом. Так ядро может управлять доступом к ресурсам железа, не полагаясь на аппаратные проверки — контролируя доступ к CPU еще при установке приложения.


В обычной OS процесс более-менее соответствует программе. Лишь некоторые программы заводят несколько процессов: к примеру, iTunes устанавливает процесс, который ждет, пока вы не вставите iPod. Но, в общем-то, практически всегда на одну программу приходится лишь один процесс, и довольно часто процесс содержит несколько программ — например, веб-браузер, запускающий внутри себя плагины.

Поскольку SIP'ы такие дешёвые, имеет смысл (и даже нужно) разбивать программы на несколько сообщающихся SIP'ов. И тут мы сталкиваемся с проблемой — как запустить программу? Что вообще такое программа, в таком случае?

Программа в Singularity описывается манифестом. Манифест это, как в .NET, XML-файл, описывающий SIP'ы из которых состоит программа, и их соединения. Манифест практически полностью генерируется автоматически из метаданных в коде. Запуская программу вы, фактически, вызываете манифест.


Драйверы это важная тема для нас, поскольку:
  1. Около 80% всех крэшей в Windows вызваны глюками в драйверах.
  2. Анализ кода Linux показал, что код драйверов в 7 раз более подвержен ошибкам, чем остальные части ядра.
  3. Когда драйвер падает, он забирает «на тот свет» всю систему, при этом теряются данные пользователя и — как следствие — его доверие.
  4. Если драйвер содержит ошибку, это может быть использовано для эскалации привилегий. А это на руку лишь «плохим ребятам».

Поскольку Singularity это микро-ядерный дизайн, основанный на системе типов, драйвер — это просто набор Sing#-классов (Sing# это слегка «прокачанный» C#), распространяемый в виде верифицируемого MSIL-байткода. Он работает в SIP, как и любая другая программа. Очевидно, что использование C# разом решает огромный класс распространённых ошибок — любой эксплойт, содержащий слово «overflow» в своём названии неприменим при использовании таких языков.

В чистом MSIL мы не можем записать специализированные инструкции, необходимые для управления железом, поэтому нам нужно какое-то другое решение. И оно выглядит, как что-то вроде небезопасной DLL, предоставляющее объекты, абстрагирующие инструкции, управляющие железом. Эта DLL ( Singularity.DriverRuntime) является частью того, что Microsoft называет «trusted computing base», что делает это похожим на слух на какую-то систему DRM, чем оно не является. Проще представить эту DLL, как логическую часть ядра, даже если оно и не работает непосредственно в контексте основного ядра. Эти классы называются IoPortRange, IoIrqRange (и т.п.) и предоставляют простой объектно-ориентированный интерфейс для программирования железа.

Singularity не даст вам создать объекты этих типов самому, поэтому вам нужно использовать специальные статические вызовы методов. И вы даже не можете вызвать их напрямую. Похоже, мы влипли — как мы получим ссылку на такой объект, если мы не можем создать их сами? И в чём смысл этой странности?

Singularity требует аннотаций вашего кода метаданными, описывая железо, для которого предназначен ваш драйвер и какие ресурсы он использует. Это делается традиционным для .NET способом — атрибутами, которыми вы помечаете ваши классы и поля. При установке приложения эти метаданные используются для построения XML-манифеста (описывающего ресурсы, необходимые вашему коду) и входных данных для статического преобразования (compile-time transform), заполняющего пустой метод, который вы предоставляете. Это преобразование само является частью ядра и применяется при установке драйвера — оно и добавляет защищенный вызов библиотеки HAL (hardware abstraction layer).

Таким образом, Singularity получает точное декларативное описание того, что именно нужно драйверу. Поскольку система гарантирует, что вы не можете получить доступ к необходимым объектам (и работать с железом) без точного указания метаданных — метаданные всегда будут указаны с предельной точностью. Это, в свою очередь, позволяет легко решать проблемы вроде конфликта драйверов и построения порядка загрузки драйверов при старте системы.


Это всё было бы очень круто, если бы кое-что (и почему всегда есть подвох?). Подвох в том, что любой драйвер, работающий с DMA-железом может обойти систему типов, перезаписывая любые адреса физической памяти.

DMA — Direct Memory Access (или прямой доступ к памяти) — оптимизация, позволяющая устройствам писать напрямую в RAM, в обход CPU. Это очень полезная штука и она реально ускоряет работу, особенно при пересылке больших объемов данных между графическими ускорителями, жесткими дисками, сетевыми картами и так далее. К сожалению, DMA идёт не только в обход CPU, но и в обход MMU, так что драйвер, контролирующий DMA-железку может контролировать и содержимое физической памяти. В обычной операционной системе это неважно, поскольку драйверы работают в режиме ядра и им полностью доверяют. Но в Singularity это проблема.

К счастью для нас, на помощь приходит DRM! Да, я не шучу! Новейшие архитектуры CPU, разрабатываемые в Intel и AMD, обладают кое-чем, называющимся (довольно удачно) IOMMU. Эта штуковина делает для железа то, что обычный MMU делает для процессора — регулирует доступ к памяти, проецируя чтение/запись по DMA через таблицы страниц. Работая с IOMMU, вы можете предотвратить несанкционированный доступ к памяти по DMA. Эта фича изначально разрабатывалась для предотвращения доступа к адресному пространству ядра через подключение железок (что является головной болью для лоббистов DRM-защиты). Ну и, разумеется, это защищает систему от глючного железа, что не такая уж и редкость.

Это также огромное подспорье для нас, потому что это закрывает дыру, через которую DMA-драйверы могли бы выбраться из своей типо-безопасной «тюрьмы».


В 2001-м году идея Microsoft об ограничениях на установку драйверов казалась нелепой. К тому моменту война браузеров подходила к концу, широкополосный Интернет только начинал распространяться и проблемы ботнетов еще не существовало. Слишком велика была вероятность неприятия а преимущества были совсем неочевидны — но, несмотря на всё это, проблема глючных драйверов была столь велика, что инициатива подписывания драйверов не могла не появиться. Microsoft предоставила бы вам пакет unit-тестов, и после успешного прохождения, этот пакет подписал бы вам драйвер. Пакет проверял множество распространенных проблем и не требовал передачи кода в Microsoft. Выглядит неплохо?

Ну, разумеется, это было спорно. Хуже, это не работало. Вместо того, чтобы проходить все тесты, некоторые разработчики сжульчинали. Чтобы не сорвать сроки или не платить $250 за верификацию, некоторые из них программно нажимали на кнопку «OK» в окошке предупреждения о неподписанном драйвере. Упс.

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

К примеру, благодаря декларативным метаданным, мы можем статически исследовать драйвер и сказать: «Ну, да, этот драйвер применим лишь для устройств класса “звуковая карта”, не использует DMA, не обращается к файлам (мы знаем это, поскольку можем статически проверить, что драйвер устанавливает соединения с файловой системой)». Что может такой драйвер сделать, кроме как накосячить со звуковой картой? Да ничего. Хоть он и может подвесить систему, неправильно программируя железо, такие баги очень редки. Он не может отправить нас в BSOD, потому что он не работает в ядре. Если он обрушится, то худшее, что может случиться — звук пропадёт до тех пор, пока драйвер не перезапустится и программы не возобновят свои соединения с драйвером.

В целом, мы можем определить некий набор привилегий (в Singularity это набор соединений, доступных SIP'у), которые могут быть опасны — и требовать подписывания только таких драйверов. Что это может быть? Доступ к системным или конфигурационным файлам, очевидно. Еще один случай это сетевые драйверы (как заDoS'ить что-то в обход файрвола? стать сетевым драйвером). К счастью, это редкость, когда драйверам требуется доступ к чему-то еще кроме железа и собственных конфигурационных данных, а сетевые драйверы неинтересны, поскольку стандартных драйверов достаточно большинству людей. Поэтому основная масса драйверов может быть безопасно написана, распространена и установлена без участия Microsoft, сохраняя при этом безопасность всей системы.