Статический анализ printf-like функций в Си при помощи libclang

от автора

По сравнению со многими современными языками язык Си зачастую кажется крайне примитивным и небезопасным. И одной из частых претензий к языку является невозможность доступа из кода в его же внутреннее представление. В других языках это традиционно осуществляется механизмами, вроде reflections, и довольно удобно в применении.

Тем не менее, с появлением libclang, можно писать собственные анализаторы и генераторы кода прямо в compile time, устраняя достаточно большое множество проблем на ранних этапах работы. Сочетание инструментов статического анализа общего плана (coverity, clang-scan), инструментов анализа для конкретного проекта, а также дисциплины написания кода позволяет намного улучшить качество и безопасность кода, написанного на Си. Конечно, это не даст гарантий, каких дает haskell или даже rust, но позволяет существенно оптимизировать процесс разработки, особенно в случае, когда переписывать огромный проект на другом языке является нереальной задачей.

В данной статье я хотел бы поделиться опытом создания плагина статического анализа format argument для функций, похожих на printf. В ходе написания плагина, мне пришлось очень много рыться в исходниках и doxygen документации libclang, поэтому я счел полезным сделать некоторый обзор для тех, кто хочет ступить на этот тернистый путь, но пока еще не уверен в целесообразности траты времени на сбор информации. В статье не будет картинок, и даже картинок блюющих единорогов, простите.

Постановка задачи

Проблема анализа printf like функций стояла у меня в проекте (https://rspamd.com) довольно давно: стандартный printf из libc не устраивал меня по многим причинам:

  • при печати в буфер, printf(3) пытается распарсить всю format string целиком, даже если она включает огромные null-terminated строки, а буфер назначения очень мал: snprintf(buf, 16, "%s", str), где str — очень длинная строка; такое поведение было мне ни к чему
  • printf крайне плохо понимает fixed length integers (uint32_t, uitn64_t)
  • хотелось печатать собственные структуры данных, например, fixed length strings без ‘\0’ в конце
  • хотелось более «продвинутых» флагов форматирования: hex encoding, human readable integers и так далее
  • хотелось уметь печатать в собственные структуры данных, например, автоматически расширяемые строки

Поэтому в свое время я взял printf из nginx и адаптировал его для своих задач. Пример кода можно посмотреть тут. У данного подхода есть один недостаток — он совершенно отключает работу стандартного анализатора query string из компилятора, а статические анализаторы общего плана неспособны понять, какие аргументы что значат. Однако эта задача идеально решается при помощи абстрактного синтаксического дерева (AST) компилятора, доступ к которому предоставляется через libclang.

Плагин обработки AST должен выполнять следующие задачи:

  • Парсинг query string и извлечение из нее всех ‘%’ аргументов
  • Сравнение количества аргументов в query string и переданных функции
  • Возможность проверки типа каждого аргумента (включая сложные типы)
  • Возможность проверки функций, которые принимают query string в разных позициях (например, printf/fprintf/snprintf)

Компиляция и работа с плагином

Несмотря на то что примеров работы с libclang в интернете достаточно, большинство из них посвящены больше анализу определений, а не анализу выражений, кроме того, почему-то множество примеров написаны на Питоне, писать на котором при наличии прекрасного (на мой взгляд) C++11 мне решительно не хотелось (хотя время компиляции прототипов на C++ — это основной серьезный недостаток).

Первой проблемой, с которой я столкнулся, было то, что разные версии llvm предоставляют разные API. Кроме того, например, osx сборка llvm, установленная через macports, оказалась неработоспособной от слова «никак». Поэтому, я просто установил llvm на свою linux песочницу и работал конкретно с этой версией — 3.7. Впрочем, данный код должен также работать и на 3.6+.

Второй проблемой оказалась система сборки. В моем проекте используется cmake, поэтому я хотел, конечно же, использовать его для построения плагина. Идея была в том, что при включенной опции собирать плагин, а затем уже использовать его для сборки остальной части кода. В первую очередь, как заведено с cmake, пришлось писать пакет для нахождения в системе llvm и libclang, расстановку CXX флагов (например, включение c++11 стандарта). К сожалению, из-за неработоспособности llvm в osx, это напрочь отломало интеграцию с замечательной IDE CLion, которую я использую для повседневной работы, поэтому писать код пришлось без дополнений и прочих удобств, предлагаемых IDE.

Компиляция плагина проблем особых не вызвала:

FIND_PACKAGE(LLVM REQUIRED)  SET(CLANGPLUGINSRC plugin.cc printf_check.cc)  ADD_LIBRARY(rspamd-clang SHARED ${CLANGPLUGINSRC}) SET_TARGET_PROPERTIES(rspamd-clang PROPERTIES             COMPILE_FLAGS "${LLVM_CXX_FLAGS} ${LLVM_CPP_FLAGS} ${LLVM_C_FLAGS}"             INCLUDE_DIRECTORIES ${LIBCLANG_INCLUDE_DIR}             LINKER_LANGUAGE CXX) TARGET_LINK_LIBRARIES(rspamd-clang ${LIBCLANG_LIBRARIES}) LINK_DIRECTORIES(${LLVM_LIBRARY_DIRS}) 

А вот с включением его для работы с остальным кодом возникли проблемы. Во-первых, cmake проявлял недюжинный искусственный интеллект, группируя зачем-то опции компилятора, превращая -Xclang opt1 -Xclang opt2 в -Xclang opt1 opt2, что напрочь ломало компиляцию. Выход нашел через прямую установку CMAKE_C_FLAGS:

IF (ENABLE_CLANG_PLUGIN MATCHES "ON")     SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Xclang -load -Xclang ${CMAKE_CURRENT_BINARY_DIR}/../clang-plugin/librspamd-clang.so -Xclang -add-plugin -Xclang rspamd-ast") ENDIF () 

Как вы видите, пришлось явно указать путь до полученной библиотеки, что потенциально ломало работу системы под osx (где используется .dylib вместо .so), но это было малозначимым фактором из-за неработоспособности llvm под osx. Второй проблемой явилось то, что если указать -Xclang -plugin, как рекоммендуется почти во всех примерах, то clang перестает компилировать исходники (то есть, он не генерирует объектные файлы), выполняя исключительно анализ. Выходом из ситуации явилась замена -Xclang -plugin на -Xclang -add-plugin, что нашлось после некоторой медитации над выдачей гугла.

Написание плагина

В данной части я не хотел бы сильно акцентировать внимание на основах создания плагинов — этому посвящено довольно много материалов. Вкратце, плагин создается при помощи статического метода clang::FrontendPluginRegistry::Add, который регистрирует плагин для clang. Данный метод является шаблонным, и он принимает тип класса, который наследуется от clang::PluginASTAction и определяет в нем нужные методы:

class RspamdASTAction : public PluginASTAction { protected:     std::unique_ptr <ASTConsumer> CreateASTConsumer (CompilerInstance &CI,             llvm::StringRef) override     {         return llvm::make_unique<RspamdASTConsumer> (CI);     }      bool ParseArgs (const CompilerInstance &CI,             const std::vector <std::string> &args) override     {         return true;     }      void PrintHelp (llvm::raw_ostream &ros)     {         ros << "Nothing here\n";     } };  static FrontendPluginRegistry::Add <rspamd::RspamdASTAction>         X ("rspamd-ast", "rspamd ast checker"); 

Основным интересным методом является метод CreateASTConsumer, который говорит clang’у, что полученный объект нужно вызвать на этапе, когда компилятор выполнил трансляцию кода в синтаксическое дерево. Вся дальнейшая работа ведется в ASTConsumer, в котором в свою очередь определен метод HandleTranslationUnit, который, собственно, получает контекст синтаксического дерева. CompilerInstance используется для управления компилятором, например, для генерации ошибок и предупреждений, что крайне удобно при работе с плагином. Целиком ASTConsumer описан так:

class RspamdASTConsumer : public ASTConsumer {     CompilerInstance &Instance;  public:     RspamdASTConsumer (CompilerInstance &Instance)             : Instance (Instance)     {     }      void HandleTranslationUnit (ASTContext &context) override     {         rspamd::PrintfCheckVisitor v(&context, Instance);         v.TraverseDecl (context.getTranslationUnitDecl ());     } }; 

Здесь мы создаем ASTVisitor, который посещает узлы дерева, и выполняем обход дерева компиляции. В данном классе, собственно, и делается вся работа по анализу вызова функций. Определен этот класс предельно просто (используя pimpl идиому):

class PrintfCheckVisitor : public clang::RecursiveASTVisitor<PrintfCheckVisitor> {     class impl;     std::unique_ptr<impl> pimpl;  public:     PrintfCheckVisitor (clang::ASTContext *ctx, clang::CompilerInstance &ci);     virtual ~PrintfCheckVisitor (void);     bool VisitCallExpr (clang::CallExpr *E); }; 

Основная мысль — наследование от clang::RecursiveASTVisitor, выполняющего обход дерева, и определение метода VisitCallExpr, который вызывается при нахождении в дереве вызова функции. В данном методе (проксированном в pimpl) выполняется основная работа по разбору функций и их аргументов. Начинается метод так:

bool VisitCallExpr (CallExpr *E) {     auto callee = dyn_cast<NamedDecl> (E->getCalleeDecl ());     if (callee == NULL) {         llvm::errs () << "Bad callee\n";         return false;     }      auto fname = callee->getNameAsString ();      auto pos_it = printf_functions.find (fname);      if (pos_it != printf_functions.end ()) { 

В данном кусочке кода, мы получаем определение (декларацию) функции из выражения и извлекаем имя функции. Дальше мы ищем в хеше printf_functions, интересует ли нас данная функция:

printf_functions = {     {"rspamd_printf",               0},     {"rspamd_default_log_function", 4},     {"rspamd_snprintf",             2},     {"rspamd_fprintf",              1} }; 

Число означает позицию query string в аргументах. Далее, если функция нас интересует, мы извлекаем query string и анализируем его (для этого я написал автомат, который несколько за рамками данной статьи):

const auto args = E->getArgs (); auto pos = pos_it->second; auto query = args[pos];  if (!query->isEvaluatable (*pcontext)) {     print_warning (std::string ("cannot evaluate query"),             E, this->pcontext, this->ci);     return false; }  clang::Expr::EvalResult r;  if (!query->EvaluateAsRValue (r, *pcontext)) {     print_warning (std::string ("cannot evaluate rvalue of query"),             E, this->pcontext, this->ci);     return false; }  auto qval = dyn_cast<StringLiteral> (         r.Val.getLValueBase ().get<const Expr *> ()); if (!qval) {     print_warning (std::string ("bad or absent query string"),             E, this->pcontext, this->ci);     return false; } 

В этом фрагменте важно то, что мы вначале пытаемся вычислить query string, если это возможно. Это полезно, например, если query string у нас формируется при помощи какого-либо выражения. К сожалению, работа со значениями в libclang делается достаточно трудно: нужно взять выражение, оценить его (EvaluateAsRValue), взять результат, который уже можно преобразовать в LValue, и далее в StringLiteral. Если вычисление не нужно, то можно брать непосредственно Expr * и приводить его к StringLiteral, что сильно упрощает код.

Далее я анализировал query string и получал вектор таких структур:

struct PrintfArgChecker { private:     arg_parser_t parser; public:     int width;     int precision;     bool is_unsigned;     ASTContext *past;     CompilerInstance *pci;      PrintfArgChecker (arg_parser_t _p, ASTContext *_ast, CompilerInstance *_ci) :             parser (_p), past (_ast), pci(_ci)     {         width = 0;         precision = 0;         is_unsigned = false;     }      virtual ~PrintfArgChecker ()     {     }      bool operator() (const Expr *e)     {         return parser (e, this);     } }; 

Каждая такая структура содержит метод вызова, который принимает аргумент (Expr *) и проверяет его тип на соответствие заданному. Дальше мы просто проверяем все аргументы после query string на соответствие типам:

if (parsers->size () != E->getNumArgs () - (pos + 1)) {     std::ostringstream err_buf;     err_buf << "number of arguments for " << fname             << " missmatches query string '" << qval->getString ().str ()             << "', expected " << parsers->size () << " args"             << ", got " << (E->getNumArgs () - (pos + 1)) << " args";     print_error (err_buf.str (), E, this->pcontext, this->ci);      return false; } else {     for (auto i = pos + 1; i < E->getNumArgs (); i++) {         auto arg = args[i];          if (arg) {             if (!parsers->at (i - (pos + 1)) (arg)) {                 return false;             }         }     } } 

Функция print_error интересна тем, что она умеет печатать ошибку компиляции и прекращать процесс компиляции. Делается это через CompilerInstance, но довольно неочевидным способом:

static void print_error (const std::string &err, const Expr *e, const ASTContext *ast,         CompilerInstance *ci) {     auto loc = e->getExprLoc ();     auto &diag = ci->getDiagnostics ();     auto id = diag.getCustomDiagID (DiagnosticsEngine::Error,             "format query error: %0");     diag.Report (loc, id) << err; } 

Соответственно, для вывода предупреждения нужно использовать DiagnosticsEngine::Warning.

Анализ типов выполняется, в целом, двумя методами. Один умеет проверять встроенные типы, например, long/int итд, а второй — сложные типы, например, структуры. Для проверки простых типов используется clang::BuiltinType::Kind, который определяет все известные клангу типы. Возможные значения можно поискать в /usr/include/clang/AST/BuiltinTypes.def (для линукса). Тут есть две тонкости:

  • Fixed size int могут по-разному совпадать с built-in type, поэтому надо делать проверки вида if (sizeof (int32_t) == sizeof (int)) {...} if (sizeof (int32_t) == sizeof (long)) {...}
  • Аргументы могут быть алиасами на другие типы, поэтому вначале их надо от этих алиасов избавить, например typedef my_int int

Итоговая функция проверки простых типов выглядит так:

static bool check_builtin_type (const Expr *arg, struct PrintfArgChecker *ctx,         const std::vector <BuiltinType::Kind> &k, const std::string &fmt) {     auto type = arg->getType ().split ().Ty;      auto desugared_type = type->getUnqualifiedDesugaredType ();      if (!desugared_type->isBuiltinType ()) {         print_error (                 std::string ("not a builtin type for ") + fmt + " arg: " +                         arg->getType ().getAsString (),                 arg, ctx->past, ctx->pci);         return false;     }      auto builtin_type = dyn_cast<BuiltinType> (desugared_type);     auto kind = builtin_type->getKind ();     auto found = false;      for (auto kk : k) {         if (kind == kk) {             found = true;             break;         }     }      if (!found) {         print_error (                 std::string ("bad argument for ") + fmt + " arg: " +                 arg->getType ().getAsString () + ", resolved as: " +                 builtin_type->getNameAsCString (ctx->past->getPrintingPolicy ()),                 arg, ctx->past, ctx->pci);         return false;     }      return true; } 

Как видно, для снятия алиасов используется метод getUnqualifiedDesugaredType, а для получения типа выражения из выражения — arg->getType(). Но данный метод возвращает qualified type (например, включая спецификатор const), что для данной задачи не нужно, поэтому qualified type разделяется split, а из получившейся структуры берется только чистый тип.

Для сложных типов необходимо выделить имя структуры, перечисления или объединения. Функция проверки выглядит так:

static bool check_struct_type (const Expr *arg, struct PrintfArgChecker *ctx,         const std::string &sname, const std::string &fmt) {     auto type = arg->getType ().split ().Ty;      if (!type->isPointerType ()) {         print_error (                 std::string ("bad string argument for %s: ") +                         arg->getType ().getAsString (),                 arg, ctx->past, ctx->pci);         return false;     }      auto ptr_type = type->getPointeeType ().split ().Ty;     auto desugared_type = ptr_type->getUnqualifiedDesugaredType ();      if (!desugared_type->isRecordType ()) {         print_error (                 std::string ("not a record type for ") + fmt + " arg: " +                         arg->getType ().getAsString (),                 arg, ctx->past, ctx->pci);         return false;     }      auto struct_type = desugared_type->getAsStructureType ();     auto struct_decl = struct_type->getDecl ();     auto struct_def = struct_decl->getNameAsString ();      if (struct_def != sname) {         print_error (std::string ("bad argument '") + struct_def + "' for "                 + fmt + " arg: " +                 arg->getType ().getAsString (),                 arg, ctx->past, ctx->pci);         return false;     }      return true; } 

Так как мы предполагаем, что аргумент у нас не структура, а указатель на нее, то вначале мы определяем тип указателя через type->getPointeeType().split().Ty. Затем выполняем desugaring и находим декларацию типа: struct_type->getDecl(). После чего проверки делаются достаточно тривиальным способом.

Результаты

Разумеется, после написания плагина я начал проверять, как он работает на своем основном коде. Были как простые проблемы с типами:

 [ 44%] Building C object src/CMakeFiles/rspamd-server.dir/libutil/map.c.o src/libutil/map.c:906:46: error: format query error: bad argument for %z arg: guint, resolved as: unsigned int                 msg_info_pool ("read hash of %z elements", g_hash_table_size                                                            ^ src/libutil/logger.h:190:9: note: expanded from macro 'msg_info_pool'         __VA_ARGS__)         ^ 1 error generated. 

так и серьезные проблемы:

 [ 45%] Building C object src/CMakeFiles/rspamd-server.dir/libserver/protocol.c.o src/libserver/protocol.c:373:45: error: format query error: bad argument 'f_str_tok' for %V arg: rspamd_ftok_t *                                         msg_err_task ("bad from header: '%V'", h->value);                                                                                ^ src/libutil/logger.h:164:9: note: expanded from macro 'msg_err_task'         __VA_ARGS__)         ^ 1 error generated. [ 44%] Building C object src/CMakeFiles/rspamd-server.dir/libstat/tokenizers/osb.c.o src/libstat/tokenizers/osb.c:128:48: error: format query error: bad string argument for %s: gsize                                         msg_warn ("siphash key is too short: %s", keylen);                                                                                   ^ src/libutil/logger.h:145:9: note: expanded from macro 'msg_warn'         __VA_ARGS__)         ^ 1 error generated. 

а также проблемы с числом аргументов:

 [ 46%] Building C object src/CMakeFiles/rspamd-server.dir/libmime/mime_expressions.c.o src/libmime/mime_expressions.c:780:3: error: format query error: number of arguments for rspamd_default_log_function missmatches query string       'process test regexp %s for url %s returned FALSE', expected 2 args, got 1 args                 msg_info_task ("process test regexp %s for url %s returned FALSE",                 ^ src/libutil/logger.h:169:30: note: expanded from macro 'msg_info_task' #define msg_info_task(...)   rspamd_default_log_function (G_LOG_LEVEL_INFO, \                              ^ 1 error generated. 

Всего было найдено 47 проблем с format query, что можно увидеть в следующем коммите: http://git.io/v8Nyv

Код плагина доступен по следующему адресу: https://github.com/vstakhov/rspamd/tree/master/clang-plugin

ссылка на оригинал статьи http://habrahabr.ru/post/270743/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *