По разным оценкам, до 10% уязвимостей в коде на C и C++ являются следствием использования неинициализированной памяти (источники: 1, 2). Задача MemorySanitizer (далее, MSAN) - выявлять использование неинициализированной памяти в коде, то есть мусора, например в блоке кода типа if (uninit_var) {...}. Кроме уязвимостей, неинициализированная память даёт о себе знать при портировании приложения на другую платформу, смене компилятора (или поднятии версии используемого), изменении уровня оптимизации или изменении кода таким образом, что то, что раньше "случайно" инициализировалось нулями, стало инициализироваться мусором.

MSAN не является статическим анализатором, то есть для его работы требуется выполнение кода (нужны тесты/fuzzing/реальная нагрузка). Прежде чем переходить к самому MSAN, сначала разберемся почему недостаточно (или достаточно?) статического анализа, ведь даже компиляторы умеют предупреждать об использовании неинициализированных данных.

Рассмотрим четыре максимально упрощенных примера кода на C с использованием неинициализированной памяти:

// trivial.c - совсем очевидный пример

int main(void) {
    int x;
    return x;
}
// prog1.c - условная инициализация переменной

int main(int argc, char** argv) {
    int x;
    if (argc % 2 == 0) {
        x = 1;
    }
    return x;
}
// prog2.c - условная инициализация переменной в другой функции

void func(int a, int *px) {
    if (a % 2 == 0) {
        *px = 1;
    }
}

int main(int argc, char** argv) {
    int x;
    func(argc, &x);
    return x;
}
// prog3.c - инициализация (части) массива с помощью memset

#include <string.h>

int main(int argc, char** argv) {
    int arr[2];
    memset(arr, 123, argc * sizeof(int));
    return arr[1];
}

Пропустим эти четыре примера через наиболее популярные статические анализаторы (где '+' означает что (возможная) проблема использования неинициализированной памяти обнаружена) :

prog3.c

prog2.c

prog1.c

trivial.c

gcc-14 -fanalyzer

-

-

+

+

clang-tidy-20

-

+

+

+

CodeQL 2.22.3

-

-

-

+

SonarScanner 7.2.0.5079

-

+

+

+

coverity 2024.12.1

-

-

+

+

pvs-studio 7.38.96703.605

-

-

+

+

infer 1.2.0

-

+

+

+

cppcheck 2.14.2

-

-

+

+

LLM Grok 4

+

+

+

+

Для Grok 4 использовался промпт: "сделай ревью кода <код целиком>" (для каждого файла отдельно). Для prog3.c (представляющий наибольший интерес, потому что ни один классический анализатор не справился) одним из пунктов ревью было: "Неопределённое поведение при чтении неинициализированных данных: Если argc=1, возврат arr[1] — UB".

Посмотреть параметры запуска анализаторов можно здесь. pvs-studio запускался локально с параметрами по умолчанию, sonarscanner запускался из sonarcloud (бесплатно для публичного проекта), для запуска coverity, даже для публичного проекта, требуется получить одобрение вендора.

Таким образом, можно сделать вывод, что наиболее популярные статические анализаторы не могут найти проблему, как минимум, в примере prog3.c, а использование нейросетей для этой задачи, хоть и показало эффективность на максимально упрощенном примере, в реальном проекте будут как ложноположительные срабатывания, так и пропуски проблем. С другой стороны, не так давно была продемонстрирована ситуация, когда ни статические анализаторы, ни санитайзеры+фаззеры не нашли реальную уязвимость, а LLM нашла. С учётом того, что LLM становятся более доступными для локального выполнения, подобная практика может применяться даже для проектов, требующих сборку в локальных контурах и запрещающих отправку кода вовне.

Также данный пример демонстрирует то, что лучше использовать несколько статических анализаторов, чем один, тем более gcc, clang-tidy, infer и cppcheck являются свободным ПО.

Используем MSAN для prog3.c (для остальных примеров аналогично):

$ clang-18 -fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g prog3.c
$ # запускаем с argc=1
$ ./a.out
==598446==WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x56309e09f4c2 in main /t/prog3.c:8:5
    #1 0x7fd3646d91c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 282c2c16e7b6600b0b22ea0c99010d2795752b5f)
    #2 0x7fd3646d928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 282c2c16e7b6600b0b22ea0c99010d2795752b5f)
    #3 0x56309e0072f4 in _start (/t/a.out+0x322f4) (BuildId: 20024e12c800da970bc354adba16c0949483c67f)

  Uninitialized value was created by an allocation of 'arr' in the stack frame
    #0 0x56309e09f469 in main /t/prog3.c:6:5

SUMMARY: MemorySanitizer: use-of-uninitialized-value /t/prog3.c:8:5 in main
Exiting
$ # запускаем с argc=2
$ ./a.out 1
$ echo $?
123

Из примера видно, что для того чтобы MSAN принес пользу нужно чтобы код выполнился по "плохому" сценарию, т.е. когда используются неинициализированные данные.

Сложности внедрения MSAN

  1. Ограничения по компилятору и ОС. MSAN поддерживается только в llvm (clang) и только на Linux, FreeBSD и NetBSD. Т.е. если ваше приложение пишется под Windows или MacOS и кросс-платформенность не нужна, то при желании использовать MSAN потребуется обеспечить сборку под одну из этих ОС и с использованием clang

  2. Все зависимости должны быть пересобраны с MSAN. Наиболее часто используемые вызовы libc перехватываются (llvm libc msan interceptors), поэтому libc пересобирать не надо. Например, перехватчик для функции getopt_long() не реализован (вероятно, потому что это GNU extension, а не POSIX). Если ее использовать без явной инициализации данных, которые передаются по указателю и задаются самой функцией getopt_long(), то возникнет ложноположительное срабатывание MSAN. В данной конкретной ситуации нет никаких препятствий явно инициализировать переменные, указатели на которые будут переданы в getopt_long(), однако это уже возможная модификация кода ради внедрения MSAN. MSAN работает как с glibc, так и с musl (по крайней мере в Alpine 3.22)

  3. Если приложение написано на C++ с использованием стандартной библиотеки, то потребуется использовать msan-инструментированную версию LLVM libc++ вместо GNU libstdc++ (или какой-то ещё реализации стандартной библиотеки которая используется). В каких-то случаях это всего лишь добавить заголовочные файлы, которые были неявно включены через другие, а в каких-то придется модифицировать код для обеспечения совместимости с libc++. Например, в libc++ начиная с LLVM-19 отсутствует (удалён) std::basic_string<unsigned char>, который до сих пор присутствует в libstdc++. Также libc++ нужно будет использовать для пересборки зависимостей, что может стать большей проблемой чем изменения в собственном коде

  4. Если используются asm-вставки, то MSAN может генерировать ложноположительные ошибки. Для решения данной проблемы требуется модифицировать код, используя __msan_unpoison() (пример)

  5. Если память инициализируется извне (например ядром), то будут ложноположительные срабатывания, требуется добавлять специальный код для MSAN (пример)

  6. MSAN замедляет работу приложения в несколько раз (и увеличивает расход памяти), что может стать проблемой при выполнении тестов (например, если тесты проверяют время выполнения и сравнивают с пороговым значением)

  7. Возможны проблемы с запуском вашего приложения (собранного с msan) из других приложений. Например, ваш проект использует cmake и для запусков тестов используется ctest. ctest обычно зависит от огромного числа библиотек, например, от libxml2 и openssl. Допустим, ваше приложение тоже зависит от libxml2, допустим вы собрали msan-инструментированный libxml2 в /opt/msan/lib и запускаете ваше приложение LD_LIBRARY_PATH=/opt/msan/lib ./your_app, если вы попробуете запустить тесты через ctest командой LD_LIBRARY_PATH=/opt/msan/lib ctest, то ctest подхватит msan-инструментированный libxml2 и не будет работать. Наиболее простые варианты решения проблемы: собирать ваш проект со статической (пересобранных с msan) линковкой библиотек или же использовать rpath для того чтобы захардкодить абсолютные пути до msan-инструментированных библиотек

  8. Если вендор ОС не публикует исходники и не предоставляет по запросу, то не получится пересобрать точно такую же библиотеку как в дистрибутиве (что не очень хорошо если в проде используются библиотеки от вендора ОС, а не собираются самостоятельно)

  9. Требуется хорошее покрытие тестами (и/или fuzzing)

Демо-пример внедрения MSAN

Рассмотрим пример проекта на C++ с использованием cmake со следующими зависимостями: стандартная библиотека C++, openssl, libcppunittest и libcppunit (для тестов). Подразумевается, что используются библиотеки из ОС(дистрибутива). План внедрения MSAN в данном случае выглядит следующим образом (исходные коды библиотек будут получены с помощью apt source в ubuntu 24.04):

Собрать libc++ с MSAN
apt source libc++-18-dev
cd llvm*/
CC=clang-18 CXX=clang++-18 cmake -G Ninja -S runtimes -B build_libcxx \
   -DCMAKE_BUILD_TYPE=Release \
   -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" \
   -DLLVM_USE_SANITIZER=Memory \
   -DLIBCXX_CXX_ABI=libcxxabi \
   -DLIBCXXABI_USE_LLVM_UNWINDER=OFF \
   -DCMAKE_INSTALL_PREFIX=/opt/msan
ninja -C build_libcxx install
Собрать openssl с MSAN (только static library)
apt source libssl-dev
cd openssl*/
CC=clang-18 \
  CFLAGS="-O2 -g -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=memory" \
  LDFLAGS="-fsanitize=memory"  \
  ./Configure no-shared no-asm no-threads no-zlib --prefix=/opt/msan
make -j && make install_sw
Собрать libcppunit с MSAN с использованием пересобранной libc++ и openssl
apt source libcppunit-dev
cd cppunit*/
PKG_CONFIG_LIBDIR="/opt/msan/lib/pkgconfig:/opt/msan/lib64/pkgconfig" \
  CC="clang-18" \
  CXX="clang++-18" \
  CFLAGS="-O2 -g -fno-omit-frame-pointer -fsanitize=memory" \
  CXXFLAGS="-O2  -g -fno-omit-frame-pointer -fsanitize=memory -I/opt/msan/include/c++/v1 -stdlib=libc++" \
  LDFLAGS="-fsanitize=memory -L/opt/msan/lib -stdlib=libc++" \
  ./configure --prefix=/opt/msan
Собрать сам проект с использованием пересобранных библиотек
mkdir build && cd build
PKG_CONFIG_LIBDIR="/opt/msan/lib/pkgconfig:/opt/msan/lib64/pkgconfig" \
  CXX="clang++-18" \
  CXXFLAGS="-O2 -g -fno-omit-frame-pointer -fsanitize=memory -I/opt/msan/include/c++/v1 -stdlib=libc++" \
  LDFLAGS="-fsanitize=memory -L/opt/msan/lib -stdlib=libc++" \
  cmake ..
make

Для запуска тестов и самого приложения потребуется использовать LD_LIBRARY_PATH, например: LD_LIBRARY_PATH=/opt/msan/lib ctest

Полный список команд в формате github workflow для демо-примера можно посмотреть здесь.

Минимизация ложноположительных срабатываний LLM (в режиме агента)

В качестве эксперимента, в Cursor (тариф Pro) с настройками по умолчанию был задан такой промпт (сам код слегка усложнён по отношению к prog3.c):

есть такой код

#include <string.h>

int main(int argc, char** argv) {
int arr[2];
memset(arr, 123, (3 - argc) * sizeof(int));
return arr[1];
}

твоя задача заключается в том чтобы разобраться в каком случае возникнет ситуация использования неинициализированной памяти, затем собрать этот пример с MemorySanitizer и убедиться что действительно есть использование неинициализированной памяти с помощью вывода msan

Нейронка "подумала" и выдала такой ответ: "Проблема возникает при argc >= 2, когда arr[1] (второй элемент) остается неинициализированным, но затем используется в return arr[1].", затем попыталась скомпилировать код с помощью clang -fsanitize=memory, это не получилось сделать потому что Cursor был запущен на MacOS, далее силами ИИ-агента была запущена сборка с msan в docker и проверено что при argc=2 (один аргумент) msan действительно сработал. Такой подход (заставить ИИ проверять свои догадки самостоятельно используя точные инструменты) выглядит намного интереснее чем самому проверять множество идей, большинство из которых будут некорректными

Заключение

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

  2. MSAN может найти больше чем статические анализаторы если "повезёт" с входными данными (хорошее покрытие тестами, fuzzing, запуск под реальной нагрузкой)

  3. Внедрение MSAN может оказаться довольно трудоёмким, прежде всего, из-за необходимости пересобрать все зависимости

  4. LLM может найти то что не находят статические и динамические анализаторы. Ложноположительные срабатывания можно уменьшить за счет верификации точным инструментом силами самой LLM, т.е. путём комбинации LLM с MSAN

Полезные ссылки

  1. Оригинальная статья, описывающая механизм работы MSAN

  2. Описание механизма работы MSAN в исходниках LLVM

  3. CWE-457: Use of Uninitialized Variable

  4. Использование MSAN в CI библиотеки libxml2

Комментарии (3)


  1. DustCn
    17.08.2025 15:06

    А как же святой грааль мемсанов - Valgrind?


    1. svl87 Автор
      17.08.2025 15:06

      по ссылке №1 (https://static.googleusercontent.com/media/research.google.com/hr//pubs/archive/43308.pdf ) есть сравнение msan (в том числе) с valgrind, если вкратце, то основной профит msan в том что он намного эффективнее по ресурсам (cpu и расход памяти) и нормально работает с многопоточными приложениями


      1. DustCn
        17.08.2025 15:06

        А почему, можете объяснить?
        Ну и не хочу придираться, в статье нет (как же это бесит) даты ее выхода. По списку литературы могу предположить что это было лет 10, если не больше назад. Не очень актуально.