The Daily Newsstand · Free, Always
Tuesday, September 22, 2026

Линтинг C stubs для OCaml с помощью Opengrep

Translate

Хотелось бы в этой статье вспомнить про такой тулинг как 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, посвященный этому языку.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.