Линтинг C stubs для OCaml с помощью Opengrep
Хотелось бы в этой статье вспомнить про такой тулинг как 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.mlmain.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, посвященный этому языку.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.