io_uring — это новый набор асинхронных механизмов, добавленный в эпоху Linux 5.x и обозначенный как будущее асинхронного режима Linux.
В этой статье мы рассмотрим ряд проблем дизайна, связанных с безопасной оболочкой io_uring в Rust, и предложим некоторые возможные решения.
Как работает io_uring
io_uring разделен на две очереди: очередь отправки SQ (очередь отправки) и очередь завершения CQ (очередь завершения). Очередь отправки содержит асинхронные задачи, ожидающие выполнения, а очередь завершения содержит события завершения.
Структура IO_URING выделяется ядром.Состояние пользователя имеет доступ к памяти к соответствующей структуре через MMAP, так что внутреннее ядерное состояние используется совместно с состоянием пользователя и обходит системный вызов двусторонней доставки данных.
Три стадии рабочего процесса
- Подготовка: приложение получает некоторые элементы очереди отправки SQE (запись очереди отправки), устанавливает каждую асинхронную задачу в каждую SQE и инициализирует ее с помощью кодов операций и параметров.
- Отправить: Приложение отправляет некоторые SQE, которые необходимо отправить в SQ, сообщает ядру, что есть новая задача через системный вызов, или позволяет ядру продолжать опрос, чтобы получить задачу.
- Сбор: приложение получает некоторые события очереди завершения CQE (событие очереди завершения) из CQ, идентифицирует и пробуждает поток/сопрограмму в приложении через user_data и передает возвращаемое значение.
epoll — это реализация модели Reactor, а io_uring — реализация модели Proactor.
Это означает, что программы, разработанные на основе epoll, трудно перенести непосредственно на io_uring.
Вопрос 1: изменение асинхронной модели не является тривиальным, если только оно не сглаживает разницу за счет некоторого снижения производительности.
вопрос 2: io_uring требует более высокой версии ядра, на этом этапе приложения должны рассмотреть, как отступить, когда функция io_uring более высокой версии отсутствует.
Ограничения для io_uring
В модели блокирующей синхронизации и модели неблокирующей синхронизации (такой как epoll) операции ввода-вывода в пользовательском режиме выполняются единовременно, не беспокоясь о продолжительности жизни.
Но io_uring — это Proactor, неблокирующая асинхронная модель с ограничениями на время жизни ресурсов.
Возьмите чтение в качестве примера, у него есть два параметра ресурса, fd и buf, При подготовке к операции ввода-вывода нам нужно заполнить SQE с fd, указателем buf и счетчиком, иГарантия того, что и fd, и buf должны быть действительны до того, как ядро завершит или отменит задачу..
fd случайная замена
fd = 6, buf = 0x5678;
准备 SQE;
close fd = 6;
open -> fd = 6;
提交 SQE;
内核执行 IO;
Перед отправкой SQE приложение «случайно» закрыло и открыло файл, что могло привести к случайному выполнению операции ввода-вывода с совершенно не связанным файлом.
Стек памяти UAF
char stack_buf[1024];
fd = 6, buf = &stack_buf;
准备 SQE;
提交 SQE;
函数返回;
内核执行 IO;
Операция ввода-вывода, выполняемая ядром, работает с памятью в стеке, которая была освобождена, что приводит к уязвимости «использование после освобождения».
Память кучи UAF.
char* heap_buf = malloc(1024);
fd = 6, buf = heap_buf;
准备 SQE;
提交 SQE;
执行其他代码出错;
free(heap_buf);
函数返回错误码;
内核执行 IO;
Выполняемый ядром ввод-вывод использует освобожденную память в куче, что является еще одной уязвимостью UAF.
использовать после переезда
struct Buf<T>(T);
let mut buf1: Buf<[u8;1024]> = Buf([0;1024]);
fd = 6, buf = buf1.0.as_mut_ptr();
unsafe {
准备 SQE;
}
提交 SQE;
let buf2 = Box::new(buf1);
内核执行 IO;
Когда ядро выполняет ввод-вывод, buf1 перемещается и указатель становится недействительным. Существует уязвимость «использование после перемещения», которая упоминается в этой статье как уязвимость UAM.
использовать после отмены
async fn foo() -> io::Result<()> {
let mut buf1: [u8;1024] = [0;1024];
fd = 6, buf = buf1.as_mut_ptr();
unsafe {
准备 SQE;
}
提交 SQE;
bar().await
}
Асинхронные функции Rust генерируют бесстековые сопрограммы, а переменные стека хранятся в структуре. Если эта структура будет уничтожена, базовый лист Future будет уничтожен, что отменит асинхронную операцию.
Однако деструкторы синхронизированы,Когда сопрограмма уничтожена, ядро все еще может занимать буфер для выполнения операций ввода-вывода.. Если не принять меры, появится уязвимость UAF.
использовать после закрытия
准备 SQE;
提交 SQE;
io_uring_queue_exit(&ring)
???
Будет ли ядро отменять выполнение ввода-вывода сразу после io_uring_queue_exit?
// TODO: найти ответ
Если она будет отменена немедленно, то программа пользовательского режима не сможет получить событие отмены, разбудить задачу или освободить ресурсы.
Если его не отменить немедленно, использование ресурсов ядром превысит время жизни экземпляра io_uring, что приведет к более неприятным проблемам.
Кажется, это говорит о том, что экземпляр io_uring должен быть статическим, чтобы жить так же долго, как и сам поток. Или взять какой-нибудь подсчет ссылок и задержать время выхода.
io_uring с Rust
Суть Rust — безопасность памяти, которая не допускает дыр в безопасности памяти или гонок данных. Правила владения Rust обеспечивают хорошую защиту от этого.
передать право собственности
«Передача права собственности» — это концепция, созданная в этой статье самостоятельно. Это означает, что для выполнения операции необходимо отказаться от права собственности на параметр, а право собственности на параметр «перенести» в другие места.
При использовании io_uring это эквивалентно тому, что ядро становится владельцем ресурса. Userland должен отказаться от контроля над ресурсом, если он не может безопасно работать одновременно. При завершении или отмене операции ввода-вывода все ресурсы, занятые ядром, будут возвращены в пользовательский режим.
Тем не менее, ядро не может на самом деле не допустить владения, на самом деле он состоит в том, чтобы пробежать асинхронно для хранения этих ресурсов и имитировать модель «Миграционную собственность».
BufRead
Черта представляет собой читаемый тип, который содержит внутренний буфер.BufReader<File>
является типичным использованием.
BufReader<File>
Может соответствовать рабочему режиму io_uring.
准备 fd, buf
准备 SQE
提交 SQE
等待唤醒
拿到返回值
回收 fd, buf
暴露 buf 的共享引用
Вопрос 3: Когда Future отменяется, баф все еще занят ядром,BufReader<File>
находится в недопустимом состоянии. При повторном выполнении IO он может выбрать только смерть.
Представьте себе такое лежащее в основе будущее
pub struct Read<F, B>
where
F: AsRawFd + 'static,
B: AsMut<[u8]> + 'static,
{
fd: F,
buf: B,
...
}
баф может быть[u8; N]
, также удовлетворитьAsMut<[u8]> + 'static
, но ему нельзя передать указатель на io_uring.
BUF недействителен, когда это будущее разрушительно и не удовлетворяет ограничениям IO_URITY.
Есть два исправления: переместите и fd, и buf в кучу перед подготовкой SQE, или ограничьте buf типом буфера, который можно безопасно экранировать.
Распределение кучи
Если вы хотите убедиться, что FD и BUF не будут разрушены перед подготовкой SQE, вы можете выделить только кучу.
Таким образом, fd и buf не будут перемещены или уничтожены до тех пор, пока операция ввода-вывода не будет завершена или отменена, что гарантирует достоверность.
pub struct Read<F, B>
where
F: AsRawFd + 'static,
B: AsMut<[u8]> + 'static,
{
state: ManualDrop<Box<State<F, B>>>
}
Однако в большинстве случаев буферы являются умными указателями на буферы динамического размера в куче, и выделение кучи для самого указателя того не стоит, и чтобы быть эффективным, вам нужно каким-то образом реализовать собственный распределитель.
побег
Обычный «анализ побега» заключается в анализе динамической области видимости объекта и размещении ее в куче, если объект может покинуть область действия функции.
Предлагаемое «побег» означает, что элементы конструкции, избежавшие разрушения, переносятся в устойчивое место.
Буферный тип безопасного выхода, когда мобильный не изменит адрес буферной памяти.
[u8;N]
полностью меняет диапазон адресов буфера при перемещении, при этомBox<[u8]>
иVec<u8>
не изменится.
SmallVec<[u8;N]>
Когда емкость не больше N, данные будут храниться в стеке, а когда она слишком велика, они будут храниться в куче.
Box<[u8]>
иVec<u8>
Поскольку буфер безопасности может сбежать,[u8;N]
иSmallVec<[u8;N]>
Не может.
Если ограничить buf типом буфера, который можно безопасно экранировать, то в самом идеальном случае для операций ввода-вывода не потребуется никаких системных вызовов, не потребуется дополнительное выделение кучи, а буфером управляет вызывающая сторона, почти идеально.
Вопрос 4: Как можно выразить это ограничение, не заражая unsafe?
Определить трейт unsafe легко и удобно, но он не является общим для всех допустимых буферов и может зависеть от правил сиротства, поэтому пользователи должны писать newtype или unsafe.
Как можно оценить, вот «безопасный побег» иPin
Понятия как-то связаны, есть ли способ их связать?
Send
Сбор io_uring может выполняться этим потоком или выделенным потоком драйвера.
В настоящее время SQ не поддерживает многопоточную отправку, и глобальный обмен должен быть заблокирован. io_uring более совместим с реализацией кольца на поток.
Рассмотрим Future, ресурсы которого уходят в кучу, когда он разрушается.
pub struct Read<F, B>
where
F: AsRawFd + 'static,
B: EscapedBufMut + 'static,
{
fd: F,
buf: B,
...
}
Если финальный деструктор выполняется глобальным потоком драйвера, ресурс будет передан из текущего потока в поток драйвера, который требует, чтобы ресурс удовлетворял требованиям Send.
Если окончательный деструктор делается этим потоком, ресурс не должен быть передан, и отправить не может быть удовлетворен.
Вопрос 5: Стратегии сбора и уничтожения также влияют на общие ограничения API. Как разработать подходящий API?
копировать
Буфер должен оставаться действительным после уничтожения Future, что означает, что мы не можем поместить временный&mut [u8]
или&[u8]
Передайте io_uring, не можете выполнять чтение или запись на месте.
Epoll FD может ждать и читать или написано, а затем прочитать или писать in situ.
В любом случае шаг помещения буфера в кучу неизбежен, разница в том, контролируется ли буфер самим асинхронным типом или вызывающим.
Позвольте вызывающему объекту управлять буфером, чтобы избежать дополнительного копирования, но это затруднит проверку безопасности, и входящий буфер должен быть ограничен, чтобы иметь хорошее поведение.
Асинхронные типы имеют встроенные буферы, которые добавляют дополнительные копии, но безопасность гарантируется автором библиотеки, что снижает вероятность ошибок.
Вопрос 6: io_uring увеличивает сложность реализации нулевого копирования в пользовательском режиме.
экология
uring-sys: привязки для liburing.
iou: низкоуровневый интерфейс io_uring в стиле Rust.
ringbahn: Экспериментальная высокоуровневая оболочка io_uring.
maglev: Экспериментальный асинхронный драйвер/среда выполнения io_uring
Суммировать
Ключ рисования
Вопрос 1: Epoll - это реализация модели реактора, а IO_IURY - это реализация модели Proactor. Изменение модели Async не является легкой задачей, если только она не разглаживает разницу со стоимостью производительности.
вопрос 2: io_uring требует более высокой версии ядра, на этом этапе приложение должно рассмотреть, как отступить, когда нет функции io_uring более высокой версии.
Вопрос 3: когда Future отменяется, buf все еще занят ядром, а асинхронный тип может находиться в недопустимом состоянии. При повторном выполнении IO он может выбрать только смерть.
Вопрос 4: Если кто-то решит ограничить buf типом буфера с безопасным экранированием, как это ограничение может быть выражено без заражения unsafe?
Вопрос 5: Стратегии сбора и уничтожения также влияют на общие ограничения API. Как разработать подходящий API?
Вопрос 6: io_uring увеличивает сложность реализации нулевого копирования в пользовательском режиме.
Если максимальная производительность не является проблемой, у нас есть различные варианты упаковки пригодной для использования библиотеки io_uring.
Если мы не принимаем во внимание общность, мы можем осторожно использовать io_uring в наших собственных программах для блокировки типа.
RUST имеет более высокую трудность для упаковки IO_URITY для безопасности, производительности и общего преследования.
ringbahnДизайнерская идея — одно из возможных направлений. Сообществу также необходимо изучить, что делает дизайн идеальным.
Расширенное чтение
Go и асинхронный ввод-вывод — мышление io_uring
Ringbahn: a safe, ergonomic API for io-uring in Rust
Ringbahn II: the central state machine
Ringbahn III: A deeper dive into drivers
feature requests: submit requests from any thread
Эта статья была впервые опубликована в колонке «Чжиху».Ржавчина каждый день"
Об авторе:
Ван Сюян, студент третьего курса, начал изучать и использовать язык Rust в 2018 году и является энтузиастом колесостроения.
GitHub ID: Nugine