• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Год I/O Ring: Что нового?

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Прошло чуть больше года с тех пор, как в Windows была представлена первая версия кольца ввода-вывода. Первоначальная версия была представлена в Windows 21H2, и я сделал все возможное, чтобы задокументировать ее здесь со сравнением с Linux io_uring здесь. Microsoft также задокументировала функции Win32. Начиная с этой первоначальной версии эта функция развивалась и получала довольно значительные изменения и обновления, поэтому она заслуживает последующего поста, в котором все они будут документированы и объяснены более подробно.

Новые поддерживаемые операции
Глядя на изменения, первое и самое очевидное, что мы видим, это то, что теперь поддерживаются две новые операции — запись и сброс:
1.png


Они позволяют использовать кольцо ввода-вывода для выполнения операций записи и сброса. Эти новые операции обрабатываются и обрабатываются аналогично операции чтения, которая поддерживается с первой версии колец ввода-вывода, и направляются соответствующим функциям ввода-вывода. В KernelBase.dll были добавлены новые функции-оболочки для постановки в очередь запросов для этих операций: BuildIoRingWriteFile и BuildIoRingFlushFile, и их определения можно найти в заголовочном файле ioringapi.h (доступен в предварительной версии SDK):
C++:
STDAPI
BuildIoRingWriteFile (
    _In_ HIORING ioRing,
    IORING_HANDLE_REF fileRef,
    IORING_BUFFER_REF bufferRef,
    UINT32 numberOfBytesToWrite,
    UINT64 fileOffset,
    FILE_WRITE_FLAGS writeFlags,
    UINT_PTR userData,
    IORING_SQE_FLAGS sqeFlags
);

STDAPI
BuildIoRingFlushFile (
    _In_ HIORING ioRing,
    IORING_HANDLE_REF fileRef,
    FILE_FLUSH_MODE flushMode,
    UINT_PTR userData,
    IORING_SQE_FLAGS sqeFlags
);

Подобно BuildIoRingReadFile, оба они создают запись очереди отправки с запрошенным кодом операции и добавляют ее в очередь отправки. Очевидно, что для новых операций необходимы другие флаги и параметры, такие как flushMode для операций сброса или writeFlags для записи. Чтобы справиться с этим, структура NT_IORING_SQE теперь содержит объединение входных данных, которые интерпретируются в соответствии с запрошенным OpCode — новая структура доступна в общедоступных символах, а также в конце этого поста.

Одно небольшое изменение ядра, которое было добавлено для поддержки операций записи, можно увидеть в IopIoRingReferenceFileObject:
2.png


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

Событие User Completion
Еще одно интересное изменение, которое было введено, — это возможность зарегистрировать пользовательское событие, которое будет уведомляться о каждой новой завершенной операции. В отличие от события CompletionEvent кольца ввода-вывода, которое сигнализируется только тогда, когда все операции завершены, новое необязательное пользовательское событие будет сигнализироваться для каждой вновь завершенной операции, позволяя приложению обрабатывать результаты по мере их записи в очередь завершения.

Для поддержки этой новой функциональности был создан еще один системный вызов: NtSetInformationIoRing:
C++:
NTSTATUS
NtSetInformationIoRing (
    HANDLE IoRingHandle,
    ULONG IoRingInformationClass,
    ULONG InformationLength,
    PVOID Information
);

Как и другие подпрограммы NtSetInformation*, эта функция получает дескриптор объекта IoRing, информационный класс, длину и данные. В настоящее время допустим только один информационный класс: 1. Структура IORING_INFORMATION_CLASS, к сожалению, отсутствует в общедоступных символах, поэтому мы не можем знать ее официальное название, но я назову ее IoRingRegisterUserCompletionEventClass. Несмотря на то, что в настоящее время поддерживается только один класс, в будущем могут поддерживаться и другие информационные классы. Здесь интересно то, что функция использует глобальный массив IopIoRingSetOperationLength для получения ожидаемой длины информации для каждого класса информации:
3.png


В настоящее время массив имеет только две записи: 0, которая на самом деле не является допустимым классом и возвращает длину 0, и запись 1, которая возвращает ожидаемый размер 8. Эта длина соответствует ожиданиям функции получить дескриптор события (HANDLEs являются 8 байт на x64). Это может быть намеком на то, что в будущем планируется больше информационных занятий, или просто другим выбором кодирования.

После необходимых проверок ввода функция обращается к кольцу ввода-вывода, дескриптор которого был отправлен в функцию. Затем, если информационный класс — IoRingRegisterUserCompletionEventClass, вызывает IopIoRingUpdateCompletionUserEvent с предоставленным дескриптором события. IopIoRingUpdateCompletionUserEvent будет ссылаться на событие и поместит указатель в IoRingObject->CompletionUserEvent. Если дескриптор события не указан, поле CompletionUserEvent очищается:
4.png


Уголок RE

С другой стороны, эта функция может выглядеть довольно большой и слегка угрожающей, но по большей части это просто код синхронизации, гарантирующий, что только один поток может редактировать поле CompletionUserEvent кольца ввода-вывода в любой точке, и предотвращающий состояние гонки. И на самом деле компилятор заставляет функцию выглядеть больше, чем она есть на самом деле, поскольку распаковывает макросы, так что если мы попытаемся реконструировать исходный код, эта функция будет выглядеть намного чище:
C++:
NTSTATUS
IopIoRingUpdateCompletionUserEvent (
    PIORING_OBJECT IoRingObject,
    PHANDLE EventHandle,
    KPROCESSOR_MODE PreviousMode
    )
{
    PKEVENT completionUserEvent;
    HANDLE eventHandle;
    NTSTATUS status;
    PKEVENT oldCompletionEvent;
    PKEVENT eventObj;

    completionUserEvent = 0;
    eventHandle = *EventHandle;
    if (!eventHandle ||
        (eventObj = 0,
        status = ObReferenceObjectByHandle(
                 eventHandle, PAGE_READONLY, ExEventObjectType, PreviousMode, &eventObj, 0),
        completionUserEvent = eventObj,
        !NT_SUCCESS(status))
    {
        KeAcquireSpinLockRaiseToDpc(&IoRingObject->CompletionLock);
        oldCompletionEvent = IoRingObject->CompletionUserEvent;
        IoRingObject->CompletionUserEvent = completionUserEvent;
        KeReleaseSpinLock(&IoRingObject->CompletionLock);
        if (oldCompletionEvent)
        {
            ObDereferenceObjectWithTag(oldCompletionEvent, 'tlfD');
        }
        return STATUS_SUCCESS;
    }
    return status;
}

Вот и все, около шести строк реального кода. Но цель этого поста не в этом, поэтому давайте вернемся к обсуждаемой теме: новому CompletionUserEvent.

Назад к событию завершения пользователя
В следующий раз мы столкнемся с CompletionUserEvent, когда запись IoRing будет завершена, в IopCompleteIoRingEntry:
5.png


В то время как обычное событие завершения кольца ввода-вывода сигнализируется только после завершения всех операций, CompletionUserEvent сигнализируется при других условиях. Глядя на код, мы видим следующую проверку:
6.png


Каждый раз, когда кольцевая операция ввода-вывода завершается и записывается в очередь завершения, поле CompletionQueue->Tail увеличивается на единицу (обозначается здесь как newTail). Поле CompletionQueue->Head содержит индекс последней записи завершения, которая была записана, и увеличивается каждый раз, когда приложение обрабатывает другую запись (если вы используете PopIoRingCompletion, оно сделает это внутренне, в противном случае вам нужно увеличить его самостоятельно). Итак, (newTail - Head)%CompletionQueueSize вычисляет количество завершенных записей, которые еще не были обработаны приложением. Если эта сумма равна единице, это означает, что приложение обработало все завершенные записи, кроме самой последней, которая сейчас заполняется. В этом случае функция будет ссылаться на CompletionUserEvent, а затем вызывать KeSetEvent, чтобы сообщить об этом.

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

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

В KernelBase.dll есть функция для регистрации события завершения пользователя: SetIoRingCompletionEvent. Мы можем найти его сигнатуру в ioringapi.h:
C++:
STDAPI
SetIoRingCompletionEvent (
    _In_ HIORING ioRing,
    _In_ HANDLE hEvent
);

Используя этот новый API и зная, как работает это новое событие, мы можем создать демонстрационное приложение, которое будет выглядеть примерно так:
C++:
HANDLE g_event;

DWORD
WaitOnEvent (
    LPVOID lpThreadParameter
    )
{
    HRESULT result;
    IORING_CQE cqe;

    WaitForSingleObject(g_event, INFINITE);
    while (TRUE)
    {
        //
        // lpThreadParameter is the handle to the ioring
        //
        result = PopIoRingCompletion((HIORING)lpThreadParameter, &cqe);
        if (result == S_OK)
        {
            /* do things */
        }
        else
        {
            WaitForSingleObject(g_event, INFINITE);
            ResetEvent(g_event);
        }
    }
    return 0;
}

int
main ()
{
    HRESULT result;
    HIORING ioring = NULL;
    IORING_CREATE_FLAGS flags;

    flags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE;
    flags.Advisory = IORING_CREATE_ADVISORY_FLAGS_NONE;
    result = CreateIoRing(IORING_VERSION_3, flags, 0x10000, 0x20000, &ioring);

    /* Queue operations to ioring... */

    //
    // Create user completion event, register it to the ioring
    // and create a thread to wait on it and process completed operations.
    // The ioring handle is sent as an argument to the thread.
    //
    g_event = CreateEvent(NULL, FALSE, FALSE, NULL);
    result = SetIoRingCompletionEvent(handle, g_event);
    thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WaitOnEvent, ioring, 0, &threadId);
    result = SubmitIoRing(handle, 0, 0, &submittedEntries);

    /* Clean up... */

    return 0;
}

Слив предшествующих операций
Событие завершения пользователя — очень классное дополнение, но это не единственное улучшение колец ввода-вывода, связанное с ожиданием. Еще один можно найти, просмотрев перечисление NT_IORING_SQE_FLAGS:
C++:
typedef enum _NT_IORING_SQE_FLAGS
{
    NT_IORING_SQE_FLAG_NONE = 0x0,
    NT_IORING_SQE_FLAG_DRAIN_PRECEDING_OPS = 0x1,
} NT_IORING_SQE_FLAGS, *PNT_IORING_SQE_FLAGS;

Просматривая код, мы можем найти проверку на NT_IORING_SQE_FLAG_DRAIN_PRECEDING_OPS прямо в начале IopProcessIoRingEntry:
7.png


Эта проверка выполняется перед любой обработкой, чтобы проверить, содержит ли запись очереди отправки флаг NT_IORING_SQE_FLAG_DRAIN_PRECEDING_OPS. В этом случае вызывается IopIoRingSetupCompletionWait для настройки параметров ожидания. Сигнатура функции выглядит примерно так:
C++:
NTSTATUS
IopIoRingSetupCompletionWait (
    _In_ PIORING_OBJECT IoRingObject,
    _In_ ULONG SubmittedEntries,
    _In_ ULONG WaitOperations,
    _In_ BOOL SetupCompletionWait,
    _Out_ PBYTE CompletionWait
);

Внутри функции есть много проверок и расчетов, которые одновременно очень технические и очень скучные, поэтому я избавлю себя от необходимости объяснять их, а вам — читать утомительное объяснение и переходить к хорошим частям. По существу, если функция получает -1 в качестве WaitOperations, она игнорирует аргумент SetupCompletionWait и вычисляет количество операций, которые уже отправлены и обработаны, но еще не завершены. Это число помещается в IoRingObject->CompletionWaitUntil. Он также устанавливает для IoRingObject->SignalCompletionEvent значение TRUE и возвращает TRUE в выходном аргументе CompletionWait.

Если функция завершилась успешно, IopProcessIoRingEntry затем вызовет IopIoRingWaitForCompletionEvent, который будет выполняться до тех пор, пока не будет передан сигнал IoRingObject->CompletionEvent. Теперь пришло время вернуться к проверке, которую мы видели ранее в IopCompleteIoRingEntry:
8.png


Если SignalCompletionEvent установлен (а это так, потому что его установил IopIoRingSetupCompletionWait) и количество завершенных событий равно IoRingObject->CompletionWaitUntil, IoRingObject->CompletionEvent получит сигнал, чтобы отметить, что все ожидающие события завершены. SignalCompletionEvent также очищается, чтобы избежать повторной сигнализации о событии, когда оно не запрошено.

При вызове из IopProcessIoRingEntry IopIoRingWaitForCompletionEvent получает тайм-аут NULL, что означает, что он будет ждать бесконечно. Это следует учитывать при использовании флага NT_IORING_SQE_FLAG_DRAIN_PRECEDING_OPS.

Итак, подведем итог: установка флага NT_IORING_SQE_FLAG_DRAIN_PRECEDING_OPS в записи очереди отправки гарантирует, что все предыдущие операции будут завершены до того, как эта запись будет обработана. Это может понадобиться в некоторых случаях, когда одна операция ввода-вывода зависит от предыдущей.

Но ожидание незавершенных операций происходит еще в одном случае: при отправке кольца ввода-вывода. В моей первой статье о кольцах ввода-вывода в прошлом году я определил подпись NtSubmitIoRing следующим образом:
C++:
NTSTATUS
NtSubmitIoRing (
    _In_ HANDLE Handle,
    _In_ IORING_CREATE_REQUIRED_FLAGS Flags,
    _In_ ULONG EntryCount,
    _In_ PLARGE_INTEGER Timeout
    );

Мое определение оказалось не совсем точным. Более правильным именем для третьего аргумента будет WaitOperations, поэтому точная подпись будет такой:
C++:
NTSTATUS
NtSubmitIoRing (
    _In_ HANDLE Handle,
    _In_ IORING_CREATE_REQUIRED_FLAGS Flags,
    _In_opt_ ULONG WaitOperations,
    _In_opt_ PLARGE_INTEGER Timeout
    );

Почему это важно? Потому что число, которое вы передаете в WaitOperations, используется не для обработки записей кольца (они полностью обрабатываются на основе SubmissionQueue->Head и SubmissionQueue->Tail), а для запроса количества операций для ожидания. Итак, если WaitOperations не равно 0, NtSubmitIoRing вызовет IopIoRingSetUpCompletionWait перед выполнением какой-либо обработки:
9.png


Однако она вызывает функцию с SetupCompletionWait=FALSE, поэтому функция фактически не будет устанавливать какие-либо параметры ожидания, а только выполнит проверки работоспособности, чтобы увидеть, допустимо ли количество операций ожидания. Например, количество операций ожидания не может превышать количество отправленных операций. Если проверка не пройдена, NtSubmitIoRing не будет обрабатывать ни одну из записей и вернет ошибку, обычно STATUS_INVALID_PARAMETER_3.

Позже мы снова видим обе функции после обработки операций:
10.png


IopIoRingSetupCompletionWait вызывается снова для пересчета количества операций, которые необходимо ожидать, принимая во внимание любые операции, которые могли быть уже завершены (или уже ожидались, если какой-либо из SQE имел упомянутый ранее флаг). Затем вызывается IopIoRingWaitForCompletionEvent для ожидания IoRingObject->CompletionEvent до завершения всех запрошенных событий.
В большинстве случаев приложения либо отправляют 0 в качестве аргумента WaitOperations, либо устанавливают его равным общему количеству отправленных операций, но могут быть случаи, когда приложение может захотеть ожидать только часть отправленных операций, поэтому оно может выбрать меньшее число для ожидания.

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

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

Мы можем найти доказательства этой ошибки при просмотре IopIoRingDispatchRegisterBuffers и IopIoRingispatchRegisterFiles. При взгляде на новую сборку мы можем увидеть фрагмент кода, которого не было в более ранних версиях:
11.png


Это проверка того, является ли процесс, который регистрирует буферы или файлы, процессом WoW64 — 32-разрядным процессом, работающим поверх 64-разрядной системы. Поскольку Windows теперь поддерживает ARM64, этот процесс WoW64 теперь может быть либо приложением x86, либо приложением ARM32.

Заглядывая дальше вперед, мы можем понять, почему эта информация важна здесь. Позже мы видим два случая, когда проверяется isWow64:
12.png


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


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


Блок слева — это корпус WoW64, а блок справа — родной корпус. Здесь мы можем увидеть разницу в смещении, к которому обращаются в переменной bufferInfo (r8 на дизассемблере). Чтобы получить некоторый контекст, bufferInfo считывается из записи очереди отправки:
C++:
bufferInfo = Sqe->RegisterBuffers.Buffers;

При регистрации буфера SQE будет содержать структуру NT_IORING_OP_REGISTER_BUFFERS:
C++:
typedef struct _NT_IORING_OP_REGISTER_BUFFERS
{
    /* 0x0000 */ NT_IORING_OP_FLAGS CommonOpFlags;
    /* 0x0004 */ NT_IORING_REG_BUFFERS_FLAGS Flags;
    /* 0x000c */ ULONG Count;
    /* 0x0010 */ PIORING_BUFFER_INFO Buffers;
} NT_IORING_OP_REGISTER_BUFFERS, *PNT_IORING_OP_REGISTER_BUFFERS;

Все подструктуры представлены общедоступными символами, поэтому я не буду приводить их все здесь, но в данном случае следует сосредоточиться на IORING_BUFFER_INFO:
C++:
typedef struct _IORING_BUFFER_INFO
{
    /* 0x0000 */ PVOID Address;
    /* 0x0008 */ ULONG Length;
} IORING_BUFFER_INFO, *PIORING_BUFFER_INFO; /* size: 0x0010 */

Эта структура содержит адрес и длину. Адрес имеет тип PVOID, и именно здесь кроется ошибка. PVOID не имеет фиксированного размера во всех системах. Это указатель, поэтому его размер зависит от размера указателя в системе. В 64-битных системах это 8 байт, а в 32-битных системах — 4 байта. Однако процессы WoW64 не полностью осознают, что работают в 64-битной системе. Существует целый механизм для эмуляции 32-битной системы, чтобы процесс позволял 32-битным приложениям нормально выполняться на 64-битном оборудовании. Это означает, что когда приложение вызывает BuildIoRingRegisterBuffers для создания массива буферов, оно вызывает 32-разрядную версию функции, которая использует 32-разрядные структуры и 32-разрядные типы. Таким образом, вместо 8-байтового указателя он будет использовать 4-байтовый указатель, создавая структуру IORING_BUFFER_INFO, которая выглядит следующим образом:
C++:
typedef struct _IORING_BUFFER_INFO
{
    /* 0x0000 */ PVOID Address;
    /* 0x0004 */ ULONG Length;
} IORING_BUFFER_INFO, *PIORING_BUFFER_INFO; /* size: 0x008 */

Это, конечно, не единственный случай, когда ядро получает аргументы размером с указатель от вызывающей программы пользовательского режима, и существует механизм, предназначенный для обработки таких случаев. Поскольку ядро не поддерживает 32-битное выполнение, эмуляция WoW64 позже отвечает за преобразование входных аргументов системного вызова из 32-битных размеров и типов в 64-битные типы, ожидаемые ядром. Однако в этом случае буферный массив не отправляется в качестве входного аргумента для системного вызова. Он записывается в общий раздел кольца ввода-вывода, который считывается непосредственно ядром, никогда не проходя через DLL-библиотеки трансляции WoW64. Это означает, что в массиве не выполняется преобразование аргументов, и ядро напрямую считывает массив, предназначенный для 32-разрядного ядра, где аргумент Length не соответствует ожидаемому смещению. В ранних версиях кольца ввода/вывода это означало, что ядро всегда пропускало длину буфера и интерпретировало адрес следующей записи как длину последней записи, что приводило к багам и ошибкам.

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

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

Прочие изменения
Есть пара небольших изменений, которые недостаточно велики или значительны, чтобы получить отдельный раздел в этом посте, но все же заслуживают почетного упоминания:
  • Успешное создание нового объекта кольца ввода-вывода приведет к созданию события ETW, содержащего всю информацию об инициализации в кольце ввода-вывода.
  • IoringObject->CompletionEvent получил повышение с типа NotificationEvent до SynchronizationEvent.
  • Текущая версия кольца ввода-вывода — 3, поэтому новые кольца, созданные для последних сборок, должны использовать эту версию.
  • Поскольку разные версии кольца ввода-вывода поддерживают разные возможности и операции, KernelBase.dll экспортирует новую функцию: IsIoRingOpSupported. Он получает дескриптор HIORING и номер операции и возвращает логическое значение, указывающее, поддерживается ли операция в этой версии.
Структуры данных
Еще одна интересная вещь произошла в Windows 11 22H2 (сборка 22577): почти все внутренние кольцевые структуры ввода-вывода доступны в общедоступных символах! Это означает, что больше нет необходимости мучительно реконструировать структуры и пытаться угадать имена полей и их назначение. Некоторые структуры претерпели серьезные изменения с 21H2, так что не нужно перепроектировать их снова и снова — это здорово.

Поскольку структуры находятся в символах, нет необходимости добавлять их сюда. Однако структуры из общедоступных символов не всегда легко найти с помощью простого поиска в Google — вместо этого я настоятельно рекомендую попробовать поиск на GitHub или просто напрямую использовать ntdiff. В какой-то момент люди неизбежно будут искать некоторые из этих структур данных, найдут структуры REd в моем старом посте, которые больше не будут точными, и будут жаловаться, что они устарели. Чтобы избежать этого хотя бы временно, я буду публиковать здесь только обновленные версии структур, которые были у меня в старом посте, но настоятельно рекомендую вам получить обновленные структуры из символов — те, что здесь, привязаны к измениться достаточно скоро (редактировать: одна сборка позже, некоторые из них уже сделали). Итак, вот некоторые структуры из сборки Windows 11 22598:
C++:
typedef struct _NT_IORING_INFO
{
    IORING_VERSION IoRingVersion;
    NT_IORING_CREATE_FLAGS Flags;
    ULONG SubmissionQueueSize;
    ULONG SubmissionQueueRingMask;
    ULONG CompletionQueueSize;
    ULONG CompletionQueueRingMask;
    PNT_IORING_SUBMISSION_QUEUE SubmissionQueue;
    PNT_IORING_COMPLETION_QUEUE CompletionQueue;
} NT_IORING_INFO, *PNT_IORING_INFO;

typedef struct _NT_IORING_SUBMISSION_QUEUE
{
    ULONG Head;
    ULONG Tail;
    NT_IORING_SQ_FLAGS Flags;
    NT_IORING_SQE Entries[1];
} NT_IORING_SUBMISSION_QUEUE, *PNT_IORING_SUBMISSION_QUEUE;

typedef struct _NT_IORING_SQE
{
    enum IORING_OP_CODE OpCode;
    enum NT_IORING_SQE_FLAGS Flags;
    union
    {
        ULONG64 UserData;
        ULONG64 PaddingUserDataForWow;
    };
    union
    {
        NT_IORING_OP_READ Read;
        NT_IORING_OP_REGISTER_FILES RegisterFiles;
        NT_IORING_OP_REGISTER_BUFFERS RegisterBuffers;
        NT_IORING_OP_CANCEL Cancel;
        NT_IORING_OP_WRITE Write;
        NT_IORING_OP_FLUSH Flush;
        NT_IORING_OP_RESERVED ReservedMaxSizePadding;
    };
} NT_IORING_SQE, *PNT_IORING_SQE;

typedef struct _IORING_OBJECT
{
    USHORT Type;
    USHORT Size;
    NT_IORING_INFO UserInfo;
    PVOID Section;
    PNT_IORING_SUBMISSION_QUEUE SubmissionQueue;
    PMDL CompletionQueueMdl;
    PNT_IORING_COMPLETION_QUEUE CompletionQueue;
    ULONG64 ViewSize;
    BYTE InSubmit;
    ULONG64 CompletionLock;
    ULONG64 SubmitCount;
    ULONG64 CompletionCount;
    ULONG64 CompletionWaitUntil;
    KEVENT CompletionEvent;
    BYTE SignalCompletionEvent;
    PKEVENT CompletionUserEvent;
    ULONG RegBuffersCount;
    PIORING_BUFFER_INFO RegBuffers;
    ULONG RegFilesCount;
    PVOID* RegFiles;
} IORING_OBJECT, *PIORING_OBJECT;

Одна структура, которой нет в символах, — это структура HIORING, представляющая дескриптор ioring в KernelBase. Он немного изменился по сравнению с 21H2, так что вот обратно спроектированная версия 22H2:
C++:
typedef struct _HIORING
{
    HANDLE handle;
    NT_IORING_INFO Info;
    ULONG IoRingKernelAcceptedVersion;
    PVOID RegBufferArray;
    ULONG BufferArraySize;
    PVOID FileHandleArray;
    ULONG FileHandlesCount;
    ULONG SubQueueHead;
    ULONG SubQueueTail;
} HIORING, *PHIORING;

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

Это одно из самых интересных дополнений к Windows 11, и, как и в любом новом фрагменте кода, в нем все еще есть ошибки, подобные той, которую я показал в этом посте. Стоит следить за кольцами ввода-вывода, чтобы увидеть, как они используются (или, может быть, злоупотребляют?), поскольку Windows 11 становится все более широко адаптированной, и приложения начинают использовать все новые возможности, которые она предлагает.

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

Перевод:
Azrv3l cпециально для xss.pro
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх