Files
progcode/editorial/agent-rewrites/029.json
T

8 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 29,
"slug": "editorial-2027-03-mechanism-d-lessons",
"title": "D и C API: как закрыть небезопасную границу буфера",
"excerpt": "Как проверить pointer, length и lifetime до вызова C, изолировать @trusted и не принять атрибут @safe за доказательство всей системы.",
"contentHtml": "<p>C-функция получает указатель и длину буфера. Указатель не несёт длину сам. Если длина пришла из заголовка пакета, а буфер содержит меньше байт, C прочитает за его пределами. Симптомы плавают: редкое падение, испорченный результат, ошибка только на одной нагрузке или незаметное повреждение памяти. Цена ошибки — потеря данных, аварийное завершение процесса и уязвимость, которую трудно связать с исходным запросом.</p>\n<p><strong>Тезис:</strong> безопасная граница D/C строится не атрибутом на имени функции. Нужны проверенный диапазон, ясный владелец памяти, известное время жизни и маленький участок, где компилятор не может проверить внешний контракт. В D этот участок обычно помечают <code>@trusted</code>. Наружу он должен отдавать интерфейс, который можно вызывать из <code>@safe</code> кода.</p>\n<h2>Как возникает ошибка</h2>\n<p>Рассмотрим условный C API. Он принимает адрес, число байт и возвращает код. C доверяет вызывающему. Он не знает capacity исходного массива и не может проверить, что <code>length</code> соответствует выделенной памяти. D тоже не восстановит этот факт из одного raw pointer.</p>\n<pre><code>extern(C) @system int decode_packet(\n const(ubyte)* data,\n size_t length\n);\n\nint call_decoder(scope const(ubyte)[] input) @trusted {\n enum headerSize = 4;\n if (input.length &lt; headerSize)\n return -1; // формат отклонён до перехода в C\n\n // Доказать отдельно: C читает только length байт\n // и не сохраняет input.ptr после возврата.\n return decode_packet(input.ptr, input.length);\n}</code></pre>\n<p>Сам вызов выглядит убедительно, но контракт неполон. Нужно знать, читает ли функция ровно <code>length</code> байт, ожидает ли завершающий ноль, сохраняет ли указатель после возврата и кто освобождает возвращённую память. Если C сохраняет адрес, передача временного массива становится ошибкой lifetime. Если формат требует заголовок фиксированного размера, проверка только верхней границы не подтверждает корректность пакета.</p>\n<h2>Что именно обещают атрибуты</h2>\n<p><code>@safe</code> ограничивает набор операций, которые могут привести к повреждению памяти. Это обещание относится к проверяемому D-коду и его интерфейсу. Оно не проверяет реализацию неизвестной C-библиотеки, её ABI, размер структуры или смысл поля.</p>\n<p><code>@system</code> разрешает низкоуровневые операции. Такой код может выполнять арифметику указателей и другие действия, которые требуют ручного доказательства. <code>@trusted</code> сохраняет эти возможности внутри тела, но разрешает вызов из безопасного кода. Поэтому <code>@trusted</code> — не знак «компилятор проверил». Это ручное обещание автора. Чем больше тело trusted-функции, тем больше непроверенных предположений в одном месте.</p>\n<p>Узкий wrapper должен принимать сильное представление входа. Slice D связывает адрес и длину. Но slice не знает, соблюдает ли внешний API null termination, не освобождает ли C память во время вызова и не сохраняет ли адрес. Эти условия остаются частью контракта библиотеки.</p><p>Практический критерий для такого wrapper простой: если функция объявлена <code>@trusted</code>, рядом должны быть названы все условия, при которых вызов C определён. В этом примере их два: диапазон ограничен переданной длиной, а указатель используется только до возврата. Не переносите эти условия в комментарий «на всякий случай»: закрепите их тестом или ссылкой на header. Если доказать условие нельзя, граница остаётся <code>@system</code>.</p>\n<figure><img src=\"/assets/editorial/2027/d-lessons-2027-constraint-matrix.svg\" alt=\"Матрица границы D и C API: размер буфера, владелец, lifetime и атрибут безопасности\" loading=\"lazy\" /><figcaption>Безопасный путь начинается после проверки длины и времени жизни. Цвет атрибута не заменяет проверку внешнего контракта.</figcaption></figure>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Падение на длинном пакете</td><td>Заявленная длина больше capacity</td><td>Сравнить длину с размером slice до вызова</td><td>Остановить вызов и вернуть ошибку формата</td></tr><tr><td>Успешный вызов, затем сбой</td><td>C сохранила указатель на временный буфер</td><td>Прочитать ownership и проверить escape адреса</td><td>Запретить сохранение или передать копию</td></tr><tr><td>Ошибка только на C-строке</td><td>Нет null terminator</td><td>Проверить завершающий байт и длину строки</td><td>Добавить terminator либо вызвать byte API</td></tr><tr><td>Работает в одном target</td><td>Не совпали ABI, alignment или layout</td><td>Сверить header, calling convention и размеры типов</td><td>Зафиксировать ABI-тест для каждого target</td></tr><tr><td>Функция помечена @trusted, но меняет всё</td><td>В wrapper спрятали парсинг и бизнес-логику</td><td>Посчитать обязанности и raw операции в теле</td><td>Оставить только boundary check и вызов C</td></tr></tbody></table>\n<h2>Минимальная проверка перед переходом в C</h2>\n<p>Проверка должна отвечать на разные вопросы отдельно. Сначала адрес принадлежит живому объекту. Затем длина целая, неотрицательная и не выходит за capacity. Потом проверяется формат: минимальный размер, terminator, допустимый enum или версия. Только после этого вызывается C-функция. Один boolean с именем <code>isValid</code> скрывает слишком много условий и плохо объясняет отказ.</p>\n<p>Ниже учебный checker. Он не анализирует D-память и не вызывает библиотеку. Он показывает отрицательный путь: неизвестный владелец и длина за пределами capacity не превращаются в <code>safe-interface</code>. Числа нужны только для иллюстрации правил.</p>\n<pre><code>struct BoundaryResult {\n string kind;\n string action;\n}\n\nBoundaryResult checkBoundary(size_t capacity, size_t length, bool pointerChecked) {\n if (!pointerChecked) {\n return BoundaryResult(`system`, `проверить указатель и владельца`);\n }\n if (length &gt; capacity) {\n return BoundaryResult(`reject`, `остановить вызов до C`);\n }\n return BoundaryResult(`safe-interface`, `передать проверенный slice`);\n}\n\n// Учебные входы: 16/8 -> safe-interface; 16/24 -> reject.\n// 16/8 без проверки указателя -> system.</code></pre>\n<p>Реальный wrapper должен учитывать переполнение при вычислении диапазона, нулевую длину, alignment, null pointer, правила потока и возвращаемый код ошибки. Если функция принимает <code>offset + length</code>, нельзя сначала сложить значения без проверки переполнения. Сравнение через <code>length &lt;= capacity - offset</code> безопаснее, если сначала доказано, что <code>offset &lt;= capacity</code>.</p>\n<h2>Ownership и lifetime нельзя угадывать</h2>\n<p>Параметр <code>scope</code> помогает выразить, что функция не должна сохранять ссылку на переданный объект. Но атрибут не переписывает документацию C. Если библиотека кладёт адрес в глобальное состояние или использует его в другом потоке, wrapper должен запретить такой сценарий или передать отдельную копию с явным владельцем.</p>\n<p>Возвращаемый raw pointer создаёт обратную задачу. До преобразования в D slice нужно знать размер объекта и способ освобождения. Если размер неизвестен, безопасного представления нет. Если C требует специальную функцию освобождения, вызов <code>free</code> из D неверен. Копирование в D-буфер часто увеличивает стоимость, но даёт ясный lifetime. Это инженерный trade-off, а не деталь синтаксиса.</p>\n<p>Не объединяйте в <code>@trusted</code> чтение файла, разбор формата, бизнес-правила и FFI. Тогда тест на один указатель не покрывает остальные решения. Пусть trusted-тело делает одну вещь: проверяет инвариант, формирует вызов и возвращает результат с понятным ownership.</p>\n<h2>Действия по порядку</h2>\n<ol><li>Выписать прототип C-функции и смысл каждого указателя, длины, возвращаемого адреса и кода ошибки.</li><li>Зафиксировать ownership: кто создаёт, кто читает, кто сохраняет и кто освобождает каждый буфер.</li><li>Проверить ABI: размер и layout структур, alignment, порядок байтов, calling convention и target-платформы.</li><li>Сформировать минимальный D wrapper со slice или копией и вынести raw операции в короткое <code>@trusted</code>-тело.</li><li>Добавить тесты на пустой вход, длину 0, точную capacity, capacity+1, null, overflow, неверный terminator и повторный вызов.</li><li>Прогнать компиляцию и runtime-проверки тем же компилятором, ABI и target, которые использует продукт; отдельно проверить sanitizer или эквивалентный инструмент.</li><li>Оставить публичную функцию <code>@safe</code> только после доказательства интерфейса. Неясную или непроверяемую ветку пометить <code>@system</code> и запретить случайный вызов.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Memory safety не означает переносимость, корректный порядок байтов, отсутствие логической ошибки или правильный ABI. <code>@safe</code> не делает C-библиотеку безопасной. <code>@trusted</code> не создаёт доказательство автоматически. Даже верная проверка capacity не замечает неверный enum, неправильную версию структуры или гонку за буфер.</p>\n<p>Если C API сохраняет входной адрес, простой вызов с borrowed slice нельзя считать готовым. Если невозможно установить размер возвращённого объекта, нужно копирование, дополнительный API или отказ от интеграции. Если target изменяет layout, один зелёный тест на локальной машине ничего не доказывает. Если неясно, кто освобождает память, не передавайте владение через границу.</p>\n<p>Код checker выше не даёт production-результата. Он не видит aliasing, реальный lifetime, alignment и calling convention. Его можно использовать только как учебную форму таблицы решений. Доказательство создают контракт библиотеки, тесты на реальном ABI и наблюдаемое поведение сборки продукта.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Граница готова, если другой инженер может по документации и коду ответить на пять вопросов: какой диапазон читается, кто владеет памятью, может ли адрес пережить вызов, какой ABI используется и как сообщается ошибка. Для каждого вопроса есть тест или явное стоп-условие. Невалидная длина не достигает C-вызова. Возвращаемый буфер освобождается тем способом, который требует библиотека.</p>\n<p>Дополнительная проверка должна проходить на всех target-платформах продукта. Успешный тест недостаточен: нужен тест, который намеренно нарушает длину, lifetime и формат и получает контролируемый отказ. Только после этого <code>@safe</code> на внешней функции описывает проверенный интерфейс, а не надежду на реализацию C.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://dlang.org/spec/memory-safe-d.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Memory Safety</a> — официально описывает категории <code>@safe</code>, <code>@trusted</code>, <code>@system</code>, роль <code>scope</code> и ограничения memory safety.</li><li><a href=\"https://dlang.org/spec/function.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Functions</a> — официальная справка по атрибутам функций и контрактам; она не проверяет lifetime конкретной C-библиотеки.</li><li><a href=\"https://dlang.org/spec/interfaceToC.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Interfacing to C</a> — официальный раздел о вызовах C из D; он не подтверждает прототип и ownership неизвестного API.</li></ul>"
}