DarkRiDDeR16 мин

D: @safe и @trusted на границе C API

DБезопасность памяти

Проблема FFI-кода появляется там, где D вызывает C-функцию с указателем и отдельной длиной буфера. Если длина пришла из другого источника, вызов может прочитать за пределами памяти, даже когда внешний метод выглядит коротким. Цена ошибки — повреждение памяти, падение процесса или уязвимость, которую трудно воспроизвести по обычному input. Один атрибут на публичной функции не исправляет неверное условие границы.

В D для этой границы различаются @safe, @trusted и @system. @safe-код ограничивает операции, которые могут привести к memory corruption. @trusted разрешает узкий участок, но ответственность за его интерфейс остаётся у автора. @system не даёт компилятору такого обещания. Механизм работает, если unsafe-код короткий, его входы проверены, а наружу выходит безопасное представление данных.

Сначала проверяем размер, потом вызываем C

C-функция часто получает pointer + length. Сам указатель не содержит длину, поэтому компилятор не может вывести, что заявленный диапазон действителен. В D безопасный wrapper должен принять массив или slice, проверить нужное условие и передать только диапазон, размер которого известен. Если C API требует null-terminated string, одного массива байт тоже недостаточно: нужна отдельная проверка завершающего байта.

Изолируйте правила владельца. Если C-функция сохраняет указатель после возврата, wrapper должен либо запретить такой вызов, либо передать копию с понятным временем жизни. Атрибут scope помогает выражать ограничения escape там, где включена соответствующая проверка, но он не заменяет договорённость с внешней библиотекой. Любая функция, которая сохраняет адрес, требует отдельного чтения API и теста.

Матрица границы D и C API: размер буфера, владелец, атрибут безопасности и допустимый результат проверки.
Схема связывает техническое ограничение с проверкой и стоп-условием. Зелёный путь начинается только после проверки длины и времени жизни.
Роли атрибутов на FFI-границе
УровеньЧто разрешаетЧто обязан проверить инженерТипичная ошибка
@safeограниченный набор операцийчто вызовы и значения остаются безопаснымисчитать весь вызванный C безопасным
@trustedузкая ручная обёрткаинвариант указателя, длины и lifetimeпоместить большой модуль в trusted
@systemнизкоуровневые операциикаждый callsite и контракт ABIпередать raw pointer без проверки
sliceуказатель и длина вместечто slice не выходит за объектдовериться внешней length

Локальная проверка буфера

Вместо вызова реальной C-библиотеки сначала можно прогнать boundary checker на данных теста. Он принимает capacity, заявленную длину и признак проверки указателя. Результат разделяет отсутствие проверки, неверный диапазон и безопасный интерфейс. Это предметный пример входа в FFI: он проверяет именно опасную пару pointer/length, а не абстрактный статус карточки.

import { checkSafeBoundary } from './upgrade-2027-03.mjs';

const calls = [
  { capacity: 16, declaredLength: 8, pointerChecked: true },
  { capacity: 16, declaredLength: 24, pointerChecked: true },
  { capacity: 16, declaredLength: 8, pointerChecked: false },
];

for (const call of calls) console.log(checkSafeBoundary(call));
// safe-interface; reject; system

Первый вход даёт диапазон внутри буфера. Второй останавливается до вызова: внешний контракт обещает 24 байта, а доступно 16. Третий не принимает решение за инженера, потому что адрес не прошёл проверку владельца. В D такой проверкой должен владеть маленький wrapper, а в тесте нужны граничные значения 0, capacity и capacity+1.

Что означает @trusted

@trusted — не «проверено компилятором». Это обещание, что внешняя форма функции безопасна, хотя тело содержит операции, которые компилятор не может проверить. Поэтому у trusted-функции должны быть короткий исходник, явные preconditions и тесты на invalid length, null, пустой slice и повторный вызов. Не прячьте в ней преобразование формата, ownership и обработку ошибок одновременно.

Если внешняя C-функция возвращает указатель, проверка должна ответить на два вопроса: объект жив и его размер известен? При ответе «нет» безопасный интерфейс невозможен без копирования или дополнительного контракта. После вызова нельзя использовать старый slice, если C-функция освобождает память. Ошибка lifetime часто переживает тесты на успешном input, поэтому негативная матрица обязательна.

Действия по порядку

  1. Прочитать C-прототип и зафиксировать смысл каждого указателя, длины, возвращаемого адреса и кода ошибки.
  2. Выделить минимальный wrapper; не переносить внутрь @trusted парсинг, бизнес-правила и сетевой код.
  3. Проверять указатель, диапазон, нулевую длину, overflow и время жизни до перехода в C.
  4. Поставить unit tests на валидные и граничные значения, затем прогнать sanitizers или инструменты платформы.
  5. Оставить публичную функцию @safe только при доказанном безопасном интерфейсе; остальную зону явно маркировать @system.

Ограничения и следующий шаг

Проверка capacity в JavaScript — учебная модель числовой границы, а не анализ D-памяти. Она не видит aliasing, alignment, calling convention, null termination или освобождение в C. Даже корректный @safe wrapper может передать семантически неверный enum или структуру. Нужен compile-time и runtime тест именно тем компилятором и ABI, с которыми собирается продукт.

Следующий шаг — выбрать один extern(C) вызов и оформить для него таблицу: pointer, length, ownership, error, thread-safety. Напишите маленький wrapper, который принимает D slice, и отдельно проведите review trusted-тела. Если один из пунктов не имеет ответа, вызов нельзя считать готовым к безопасной границе.

Проверяемые источники

  • D Language Specification: Memory Safety — версия и дата: D language specification, page generated 23 July 2026. Применение: Определения @safe, @trusted, @system, scope и границы memory safety. Граница: Спецификация не проверяет контракт внешней C-библиотеки и не гарантирует portability или отсутствие логических ошибок.
  • D Language Specification: Functions and Function Safety — версия и дата: D language specification, page generated 25 July 2026. Применение: Правила function attributes и contract expressions используются для precondition и postcondition. Граница: Документация не создаёт unit tests и не решает lifetime конкретного объекта.
  • D Language Specification: Interfacing to C — версия и дата: D language specification, page checked 31 July 2026. Применение: Интерфейс D/C и отдельные соглашения вызова используются для описания wrapper boundary. Граница: Страница не подтверждает прототип, ABI и ownership неизвестной библиотеки.