Yo!
Хотелось бы в этой статье вспомнить про такой тулинг как Semgrep и его hard‑fork в лице Opengrep на примере специфичной, но подходящей задачи — проверки корректности написанных C stubs для работы с runtime'мом OCaml'а.
Вам мало «man 1 grep»?
Semgrpe — это статический анализатор кода и платформа для нахождения багов, уязвимостей и других security моментов. И работает оно просто как grep, то есть ищет участки кода, соответствующие описанным правилам, но в отличие от grep, работающего с текстом, Semgrep работает с деревьями абстрактного синтаксиса (AST), нивелируя условности форматирования исходного кода.
Давайте для демонстрации напишем простенький пример, обыкновенный «Hello world»
let () = print_endline "Hello, Harry!"
в нем мы станем ловить вызов функции print_endline и фиксить ее аргумент. Для этого опишем следующее правило в файле rule.yml
rules: - id: match-print_endline languages: [ocaml] severity: INFO pattern: print_endline $ARG message: Пойман $ARG! fix: "Hello, World!"
Для запуска достаточно написать теперь:
$ opengrep scan --config rule.yml main.ml
main.ml ❱ my-first-rule Пойман "Hello, Harry!"! ▶▶┆ Autofix ▶ "Hello, World!" 1┆ let () = print_endline "Hello, Harry!"
В общем вы должны были понять, что оно позволяет ловить в коде какие‑либо плохие практики и нарушения, нерегулирыемые в рамках абстракций самого языка программирования. Конечно, это не заменяет настоящий взрослый синтаксический анализ общего назначения, но все равно остается достаточно интересным решением.
C stubs для OCaml
Возможно OCaml вам может показаться слишком экзотичным, но те же проблемы вы можете встретить при попытках взаимодействия с разными другими runtime'ами более «традиционных» языков.
Далее я приведу парочку примеров, со остальными вы сможете ознакомиться в репозитории. По части синтаксиса паттернов рекомендую обратиться к официальной справке.
Ловим сохранение значений, что могут быть перемещены GC
Когда мы пишем stubs мы должны держать в голове, что значения могут быть перемещены сборщиком мусора в независимости от нашего желания, поэтому нам не следует сохранять ссылки на значения, что могут быть перемещены далее по коду.
- id: ocaml-c-bindings-properly-values-saving languages: [c] severity: WARNING message: > OCaml value '$NAME' may be moved or collected by the GC before it is used. metadata: category: correctness technology: [ocaml, c] patterns: - pattern-either: - pattern: $TY $NAME = String_val(...); - pattern: $NAME = String_val(...); - pattern-inside: $RET $FUNC(...) { ... }
const char *text = String_val(ocaml_string); /* This allocation could trigger the OCaml garbage collector. */ value result = caml_alloc_string(strlen(text)); memcpy(String_val(result), text, strlen(text));
❯❱ ocaml-c-bindings-properly-values-saving OCaml value 'text' may be moved or collected by the GC before it is used. 6┆ const char *text = String_val(ocaml_string);
Обозначение блокирующей секции и парности функций
На самом деле этот код можно было бы заменить макросом, но такого макроса нет, поэтому нету гарантий, что кто‑то так будет делать, но тут мы ловим пару функцию: функция входа в блокирующий блок и выход из блока.
- id: ocaml-c-bindings-unpaired-runtime-blocking-section languages: [c] severity: CRITICAL metadata: category: correctness technology: [ocaml, c] message: > caml_enter_blocking_section() must be paired with caml_leave_blocking_section() in the same function. patterns: - pattern: caml_enter_blocking_section(); - pattern-inside: $RET $FUNC(...) { ... } - pattern-not-inside: | $RET $FN(...) { ... caml_leave_blocking_section(); ... }
caml_enter_blocking_section(); sleep(2);
❯❯❯❱ ocaml-c-bindings-unbalanced-runtime-blocking-section caml_enter_blocking_section() must be paired with caml_leave_blocking_section() in the same function. 5┆ caml_enter_blocking_section();
Ловля аллокации внутри блокирующей секции
Во время блокирующего блока, который освобождает runtime, мы не можем вызывать alloc‑функции, кидать исключения и так далее, иначе могут возникнуть критические нарушения работы.
- id: ocaml-c-bindings-runtime-use-in-blocking-section languages: [c] severity: CRITICAL metadata: category: correctness technology: [ocaml, c] message: > Do not allocate or access OCaml values inside a blocking section. Convert OCaml values to C data before caml_enter_blocking_section() and create OCaml values only after caml_leave_blocking_section(). patterns: - pattern-inside: | caml_enter_blocking_section(); ... caml_leave_blocking_section(); - pattern-either: - pattern: caml_alloc(...); - pattern: caml_alloc_small(...); - pattern: caml_alloc_shr(...); - pattern: caml_alloc_tuple(...); - pattern: caml_alloc_string(...); # ...
caml_enter_blocking_section(); s = caml_copy_string("Hello"); caml_leave_blocking_section();
❯❯❯❱ ocaml-c-bindings-runtime-use-in-blocking-section Do not allocate or access OCaml values inside a blocking section. Convert OCaml values to C data before caml_enter_blocking_section() and create OCaml values only after caml_leave_blocking_section(). 8┆ s = caml_copy_string("Hello");
Ловим потенциальный memory leak
Если мы хотим использовать какой‑либо сторонний объект из C кода внутри OCaml нам следует его обвернуть в OCaml прокси‑значение. И тут мы может возникнуть ситуация с потенциальной утечкой памяти, если alloc‑функция выбросит исключения, поэтому следует сначала выделить память под OCaml значение, а потом уже выделять C значение. Тогда в случае преждевременного выхода из функции у нас не потеряется, не утечет, выделенная память.
- id: ocaml-c-bindings-resource-leak-before-ocaml-custom-alloc languages: [c] severity: WARNING metadata: category: correctness technology: [ocaml, c] message: | Potential resource leak before OCaml custom block allocation. A C resource (handle, pointer, context) is allocated/created before caml_alloc_custom is called. If caml_alloc_custom raises an exception (e.g., Out_of_memory), the OCaml runtime performs a longjmp, bypassing C cleanup code, which leaks the acquired resource. How to fix: Allocate the custom block first (with NULL), then acquire the resource. patterns: - pattern-inside: $RET $FUNC(...) { ... } - pattern-either: - pattern: | $C_TYPE *$C_VALUE = $ALLOC_C_VAL_FUNC(...); ... $NAME = caml_alloc_custom($OPS, sizeof($C_TYPE *), ...); ... $MACRO($NAME) = $C_VALUE;
void *device = open_device(String_val(path)); value result = caml_alloc_custom( &device_ops, sizeof(void *), 0, 1 ); Custom_resource(result) = device;
❯❱ ocaml-c-bindings-resource-leak-before-ocaml-custom-alloc Potential resource leak before OCaml custom block allocation. A C resource (handle, pointer, context) is allocated/created before caml_alloc_custom is called. If caml_alloc_custom raises an exception (e.g., Out_of_memory), the OCaml runtime performs a longjmp, bypassing C cleanup code, which leaks the acquired resource. How to fix: Allocate the custom block first (with NULL), then acquire the resource. 4┆ void *device = open_device(String_val(path)); 5┆ 6┆ value result = caml_alloc_custom( 7┆ &device_ops, 8┆ sizeof(void *), 9┆ 0, 10┆ 1 11┆ ); 12┆ 13┆ Custom_resource(result) = device;
Этот пример самый показательный по части использования мета‑переменных.
Ты пойман, unrooted!
Последнее, что покажу и что оказывается самым полезным, это проверка, что все OCaml значения у нас зарегистрированы в текущем scope. Ибо из-за человеческого фактора мы можем забыть, потерять или даже не думать об этом, но это очень важный момент. Компилятор тут не подскажет, а вот анализатор укажет.
- id: ocaml-c-bindings-unrooted-local-value languages: [c] severity: WARNING message: > Local OCaml value '$NAME' may be unrooted. Register it with CAMLlocal1, CAMLlocal2, etc. before calling allocating OCaml C APIs. metadata: category: correctness technology: [ocaml, c] patterns: - metavariable-pattern: metavariable: $ALLOC_FUNC pattern-either: # ... - pattern-either: - pattern: value $NAME; - pattern: $NAME = $ALLOC_FUNC(...); - pattern: value $NAME = $ALLOC_FUNC(...); - pattern-inside: $RET $FUNC(...) { ... } - pattern-not-inside: $RET $FUNC(...) { ... CAMLlocal1($NAME); ... } - pattern-not-inside: $RET $FUNC(...) { ... CAMLlocal2(..., $NAME, ...); ... } - pattern-not-inside: $RET $FUNC(...) { ... CAMLlocal3(..., $NAME, ...); ... } # .... - id: ocaml-c-bindings-unrooted-params-value languages: [c] severity: WARNING metadata: category: correctness technology: [ocaml, c] message: > OCaml value parameter '$NAME' is not registered with CAMLparam*. Register value parameters before calling code that may trigger the garbage collector. patterns: - pattern-inside: $RET $FUNC(..., value $NAME, ...) { ... } - pattern-not-inside: $RET $FUNC(..., value $NAME, ...) { ... CAMLparam1($NAME); ... } - pattern-not-inside: $RET $FUNC(..., value $NAME, ...) { ... CAMLparam2(..., $NAME, ...); ... } - pattern-not-inside: $RET $FUNC(..., value $NAME, ...) { ... CAMLparam3(..., $NAME, ...); ... } # ...
CAMLprim value make_pair_unrooted(value first, value second) { value result = caml_alloc_tuple(2); Store_field(result, 0, first); Store_field(result, 1, second); return result; }
❯❱ Users.dx3mod.Projects.semgrep-ocaml-c-rules.ocaml-c-bindings-unrooted-params-value OCaml value parameter 'first' is not registered with CAMLparam*. Register value parameters before calling code that may trigger the garbage collector. 1┆ CAMLprim value make_pair_unrooted(value first, value second) ❯❱ ocaml-c-bindings-unrooted-params-value OCaml value parameter 'second' is not registered with CAMLparam*. Register value parameters before calling code that may trigger the garbage collector. 1┆ CAMLprim value make_pair_unrooted(value first, value second) ❯❱ ocaml-c-bindings-unrooted-local-value Local OCaml value 'result' may be unrooted. Register it with CAMLlocal1, CAMLlocal2, etc. before calling allocating OCaml C APIs. 3┆ value result = caml_alloc_tuple(2);
Итого
В общем такой кейс у меня был. Если вы были не знакомы с Semgrep/Opengrep очень рекомендую познакомиться. У него есть Registry с правилами и как видите, очень просто добавить свои правила.
Конечно, сейчас все больше и больше применяют ИИ для поиска багов и других проблем в коде, и ИИ оказывается куда способнее и покрывает больше, чем я самостоятельно «настругаю» правила, но это очень толстый pipeline, и поэтому для таких легковесных решений как Opengrep до сих пор остается места в практике.
Буду рад, если вы поделитесь своим опытом использования этим инструмент, а если вы вдруг заинтересуетесь упомянутым языком программирования OCaml'ом, то приглашаю на наш русско‑язычный веб‑сайт OCaml Portal, посвященный этому языку.
Всем спасибо за внимание:<