Как мы пишем код для curl на C

Мне часто задают такой вопрос: как мы пишем на C код для curl, чтобы он был безопасным и надёжным в миллиардах установок? Мы предпринимаем определённые меры и принимаем решения. «Серебряной пули» нет, есть только рекомендации. Как вы убедитесь сами из этой статьи, в них тоже нет ничего странного или неожиданного.

«c» в слове «curl» не обозначает и никогда не обозначало язык программирования C, это расшифровывается как client.

Предупреждение

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

Тестирование

Мы пишем столько текстов, сколько возможно. Мы очень часто пропускаем наш код через все возможные инструменты статического анализа. Мы безостановочно тестируем код фаззерами.

C небезопасен по памяти

Мы совершенно точно не защищены полностью от багов, ошибок и уязвимостей, связанных с памятью. По нашим подсчётам, примерно 40% наших ранее найденных уязвимостей безопасности стали прямым следствием использования C вместо безопасного по памяти языка. Однако этот показатель всё равно намного меньше, чем 60-70%, о которых часто говорят некоторые крупные компании и проекты. Не могу сказать, вызвано ли это различиями в ведении статистики или действительно тем, что у нас возникает меньше проблем с языком C.

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

На данный момент у нас примерно 180 тысяч строк кода продакшена на C89 (не считая пустых строк). Мы продолжаем использовать C89 для обеспечения максимальной портируемости и потому, что верим в идеал постоянных непрерывных итераций и совершенствования без переписывания кода.

Читаемость

Код должен быть удобным для чтения. Он должен быть понятным. Нельзя прятать код под хитрыми конструкциями, сложными макросами или перегрузками. Удобочитаемый код легко анализировать, легко отлаживать и легко дополнять.

Маленькие функции проще читать и понимать, чем длинные, поэтому они предпочтительнее.

Код должен читаться так, как будто он написан одним человеком. Весь код должен придерживаться согласованного и однородного стиля, потому что это упрощает его чтение. Ошибочный или несогласованный стиль оформления кода — это баг. Мы устраняем все найденные баги.

У нас есть инструментарий для проверки базового соответствия стилю оформления кода.

Узкий код и короткие имена

Код должен писаться узко. Глазу сложно читать длинные строки, поэтому у нас есть строгое ограничение на длину строки в 80 символов. Мы используем отступы в два пробела, и это позволяет нам использовать определённые уровни отступов до того, как ограничение на длину столбца становится проблемой. Если проблемой становится уровень отступов, то, вероятно, код стоит разбить на несколько подфункций?

Связанное с этим правило: идентификаторы и имена (в особенности локальные) должны быть короткими. Длина имён усложняет их чтение, особенно если есть множество похожих имён. Не говоря уже о том, что их может быть сложно уместить в 80 столбцов после достижения определённого уровня отступов.

Многие шутят о том, что давно уже продаются широкие экраны, но самое главное здесь — это читаемость. Широкий код сложнее читать. Вопрос заключается в том, какое ограничение установить, и подобные обсуждения есть в каждом проекте.

Отсутствие предупреждений

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

Избегать «плохих» функций

Есть некоторые функции C, которые мы считаем просто плохими из-за отсутствия у них контроля границ или локального состояния, поэтому избегаем их (gets, sprintf, strcat, strtok, localtime и так далее).

Есть другие функции C, сложные по иным причинам. Например, они могут быть слишком «свободными» или выполнять задачи, которые часто оказываются проблематичными или вовсе ошибочными; их использование запросто привело бы нас к просчётам. По этим причинам мы избегаем sscanf и strncpy.

У нас есть инструментарий, запрещающий использование этих функций в коде. Если попытаться добавить какую-нибудь из них в пул-реквесте, то задачи CI «замигают красным» и предупредят автора о его ошибке.

Функции работы с буферами

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

Кроме того, для каждого динамического буфера задан максимальный размер; это простое решение, в то же время позволяющее отлавливать ошибки. В современном коде libcurl у нас есть примерно восемьдесят динамических буферов.

Функции парсинга

Выше я говорил, что мы не любим sscanf. Это мощная функция для парсинга, но часто оказывается так, что она парсит больше, чем нужно пользователю (например, больше одного пробела, даже если допустим может быть только один); к тому же у неё слабая (отсутствующая) обработка переполнений integer. А ещё она стимулирует пользователей без необходимости копировать распарсенные результаты, что приводит к излишнему использованию локальных стековых буферов или короткоживущих распределений кучи.

Вместо неё мы создали ещё одно множество вспомогательных функций для парсинга строк и постепенно перейдём в коде парсера curl на применение этого множества. Оно упрощает написание строгих парсеров, выполняющих ровно то, что хочет пользователь, позволяет избегать лишнего копирования/malloc и лучше справляется со строгой обработкой переполнений integer и проверками границ.

Мониторинг использования функций работы с памятью

Проблемы с памятью часто связаны с динамическим распределением памяти, за которым следует копирование данных в распределённую область памяти. Если распределение и копирование выполнены корректно, то проблем не возникает, но если что-то из них поведёт себя неправильно, то всё может пойти плохо. Поэтому мы стремимся минимизировать применение этого паттерна. Мы предпочитаем strdup и дупликацию памяти, выполняющую распределение и копирование данных в одном вызове, или вспомогательные функции, выполняющие эти задачи под своими API. В дэшборде curl мы ведём ежедневно обновляемый график плотности вызовов функций работы с памятью. В идеале кривая на графике со временем должна снижаться.

Как мы пишем код для curl на C - 1

Возможно, стоит также добавить, что мы стараемся не использовать чрезмерно распределения памяти, особенно на горячих путях выполнения кода. Большому объёму скачивания не требуется больше распределений, чем маленькому.

Тщательная проверка умножений

Ещё один источник проблем переполнения integer. Каждая выполняемая арифметическая операция должна обеспечивать невозможность переполнения. К сожалению, это по большей мере приходится делать вручную и анализировать самостоятельно.

Гарантированная поддержка 64 битов

В начале 2023 года мы отказались от поддержки сборки curl в системах без 64-битного целочисленного типа. Это сильно упрощает код и логику. Понижается вероятность переполнений integer и отсутствует риск того, что авторы будут рассчитывать на 64-битную арифметику, которая окажется 32-битной в каких-то редких сборках, как это бывало в прошлом. Разумеется, при выборе неподходящего типа всё равно возможны переполнения и ошибки.

Максимальная длина строк

Чтобы предотвратить возникновение ошибок при работе со строками, в особенности с переполнениями integer, но и с другой логикой, у нас есть общая проверка всех входящих в библиотеку строк: она не принимает строки длиннее заданного ограничения. Мы считаем любое множество строк большей длины или ошибкой, или попыткой (атакой?) вызвать что-то странное внутри библиотеки. При таких вызовах мы возвращаем ошибку. Сегодня это максимальное ограничение равно восьми мегабайтам, но в будущем, с развитием curl, мы можем увеличить его.

master остаётся золотым стандартом

Ломать master не допускается ни в коем случае. Мы мерджим в master код, только если считаем его чистым, правильным и идеально работающим. Иногда мы допускаем оплошности, но стремимся как можно быстрее реагировать на такие ситуации.

Всегда проверять ошибки и реагировать на них

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

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

API и ABI

Каждая публично доступная функция или интерфейс никогда не должна меняться при риске поломки API или ABI. По этой причине и для того, чтобы упростить выявление функций, требующих этих предостережений, у нас есть строгое правило: у публичных функций есть префикс curl_, и никакие другие функции его не используют.

Это может делать каждый

Благодаря процессу код-ревью, множеству автоматических инструментов, а также подробному и обширному набору тестов писать код для curl может (попробовать) каждый. Разумеется, если он знаком с языком C. Опасность того, что проблемы окажутся незамеченными, приблизительно одинакова, кем бы ни был автор кода. Мы все делим эту ответственность.

Так что вперёд. Вы сможете!

Автор: PatientZero

Источник

Оставить комментарий