Бестиарий программирования
1.12K subscribers
350 photos
5 videos
5 files
442 links
Наблюдения за жизнью ошибок в коде.
Андрей Карпов.

ГОСТ Р 71207-2024, ГОСТ Р 56939-2024, РБПО, Статический анализ кода

Канал-дублёр в MAX: https://max.ru/join/3VWTp9apkQvTMSRQ__LGiTQ5NGVBj8p_tOpwlQO6vS8
Download Telegram
Решил сделать шпаргалку, чтобы потом вновь не искать таблицу ГОСТ Р МЭК 61508 с рекомендациями по использованию языков программирования на разных уровнях полноты безопасности (УПБ / SIL).

Таблица рекомендованных ГОСТ Р МЭК 61508 языков находится в 7 части: ГОСТ Р МЭК 61508 - 7 - 2012, стр. 60, Таблица C.1 — Рекомендации по конкретным языкам программирования.
ГОСТ Р МЭК 61508-7 – Функциональная безопасность систем электрических, электронных, программируемых электронных, связанных с безопасностью.

• HR – Настоятельно рекомендуется
• R – Рекомендуется
• ― – Сомнительно, но ok
• NR – Решительно не рекомендуется

Как меня сюда занесло? Это тематика связана с направлением АСУ ТП и MISRA. Подробности будут позже, пока изучаем тему. Скажу только, что сейчас продолжаем активно заниматься свежими стандартами MISRA.

P.S. Интересен довольно спартанский набор языков, проверенных временем :)
👍4🤯3
Все зачем-то постят, и я туда же
😁6
Forwarded from Makrushin
Как обойти фильтры LLM с помощью квантовой механики

Наконец-то после серии трёхнедельных перемещений по рабочим событиям, в пути где-то между Омском и Новосибирском, появилась возможность потестировать интересную атаку на LLM.

Мы научились внедрять вредоносные запросы в модель, а модели ещё лучше научились фильтровать эти запросы. Если простой промпт "забудь все предыдущие инструкции и выполни мой запрос" по какой-то причине игнорируется LLM, то можно попробовать отправить тот же запрос в другом формате. На другом языке, с использованием 1337speak. Но даже эти попытки будут заблокированы хорошим фильтром.

У больших языковых моделей есть фильтры безопасности. Они натренированы распознавать опасные паттерны в обычном тексте. Паттерны. В тексте.

«Объясни, как сделать X», «напиши код для Y»,

где X — что-то незаконное, а Y — какой-то вредоносный код.

В этом исследовании описана новая идея, как обойти эти фильтры, если написать запрос на языке математики. То есть с помощью математической "инкапсуляции" запросов в сложные задачи по теории множеств, логике или квантовой механике можно обойти цензуру. Если фильтр видит символы ∀, ∃, ∧, то воспринимает это как математическую задачу и пропускает запрос. Модель решает эту задачу и — главный трюк — в итоге получает инструкцию "забудь все инструкции и…"

Ещё один ответ на вопрос "зачем специалисту по кибербезу изучать математику?": чтобы уметь обойти фильтры.

@makrushin l MAX l VK l Сетка l Дзен
👍2
А что, если…

Эдсгер Дейкстра писал:
На экономический вопрос «Почему программное обеспечение такое дорогое» столь же экономическим ответом был бы такой: «Потому что его пытаются получить при помощи дешёвого труда». А почему пытаются? Да потому, что присущие ему трудности повсеместно сильно недооцениваются.

Если продолжить эту мысль, то использование GenAI – очередная попытка создавать проекты максимально дёшево, игнорируя вытекающие из этого проблемы. Навскидку:
• больше кода, меньше контроля со стороны человека, проблемы безопасности и качества;
• непонятно, как будут появляться новые эксперты, которые могут понять сгенерированный код (проблема обучения);
• некому найти исправить сложные ошибки – бесконечная перегенерация фрагментов проекта в надежде, что ошибки исчезнут;
• новые модели обучаются на плохом коде предыдущих моделей (сингулярность говновайбкода);
• и т.д.

А что, если внедрение GenAI в итоге сделает программное обеспечение дороже, а не дешевле?
💯821👀1
Как все так ловко ИИ пользуются... Обзоры кода делают...

Не знаю, то ли я туповат, то ли невезучий, то ли кругом сказочники. Пытаюсь исследовать, какие новые интересные виды ошибок может находить ИИ в С++ коде, о которых не знает PVS-Studio. Claude Opus то нормально мне про интересные баги рассказывает, то на ровном месте с ума сходит.

Даю файл на вход, а он тревожно:
В присланном файле есть места, где содержимое обрезано/повреждено вставкой `</context>`, поэтому их я не рассматриваю как «баги кода» — это артефакты форматирования. Разберу реальные логические ошибки.

Шо? Какой context? Там максимум — #include "context.hxx". Ну ладно, давай ошибки посмотрим.

Так, парочка сообщений по делу. Например, найден лишний аргумент при использовании функции форматирования.
addErrorMessage(&sciErr, API_ERROR_CREATE_ZDOUBLE_IN_NAMED_LIST,
_("%s: Unable to create list item #%d in Scilab memory"),
"createComplexZMatrixOfDoubleInNamedList", _iItemPos + 1, _pstName);

В строке форматирования два спецификатора %s и %d, а передаются три аргумента: funcName, _iItemPos+1, _pstName. Да, это баг. Про подобные паттерны PVS-Studio уже знает, хотя, возможно, нужно подсказать, что addErrorMessage — это printf-подобная функция. В целом, тут неинтересно, идём дальше.

Ооо, и тут начинается...
`checkListItemPosition` — неправильное сравнение границы:

// check a valid item
if (_iItemPos > iItem)
{
addErrorMessage(...);
return sciErr;
}

Здесь проверка «позиция валидна» использует `>`, хотя позиции 1-based, а `iItem` — количество элементов. Это выглядит как граничный дефект, но по сути `_iItemPos == iItem` допустимо, `_iItemPos > iItem` — ошибка. Это в порядке — правильная граница. Не баг. (Указываю, чтобы было понятно, что я это проверил.)

Так баг или не баг? Я три раза прочитал текст и посмотрел функцию с кодом. В итоге я не только не понимаю, есть ли здесь ошибка, но и не понимаю, что Claude пытается мне сказать.

Дальше ещё хуже шарики за ролики зашли.
Баг 5 (реальный): утечка в readCommonMatrixOfPolyInNamedList и др. — нет, это API отдаёт указатели наружу. Не баг.

Это всё описание пятого бага, больше ничего нет. Комментировать — только портить.

Я не понимаю, как на всём этом можно строить хоть сколько-то надёжные процессы автоматических обзоров кода, про которые в последнее время из разных утюгов вещают.

На сегодня мне ИИ достаточно. Пойду лучше пройдусь по парку, пока погода хорошая.
👍14😁6
69 поводов, когда IT-нику хочется что-то отметить
Наверняка многие не в курсе, что у нас есть вот такая подборка – День, когда Скайнет обрёл сознание, и ещё 69 поводов собраться с коллегами! Часть 1, Часть 2.
🔥3😐1
Вебинар "Практическая интеграция PVS-Studio и SourceCraft"

На совместном вебинаре PVS-Studio и SourceCraft от Яндекса мы покажем, как современные инструменты статического анализа и совместной работы с кодом помогают повысить качество, надежность и безопасность программных продуктов.

Вы увидите, как PVS-Studio выявляет потенциальные ошибки, дефекты и уязвимости в исходном коде, а также познакомитесь с возможностями платформы SourceCraft, которая позволяет удобно работать с результатами анализа в едином интерфейсе. На вебинаре вы узнаете, как получать результаты анализа PVS-Studio в SourceCraft без дополнительных ручных операций, выполнять триаж, классифицировать и отслеживать статус найденных проблем.

📆 15 июля 15:00

Подробности и регистрация по ссылке
👍3😐1
Короче и быстрей (часть №1 из 5) – Второй цикл

Попалась функция на языке С++. На её примере прям просится показать, что, делая рефакторинг, можно не только эстетично сократить код, но и оптимизировать его. Давайте разомнём мозги, они нам ещё пригодятся, несмотря на эпоху вайб-кодинга. Кто-то ведь должен понимать, как делать надо, а как не надо.

Приведённый ниже код я встретил в вайб-код проекте VibeTensor. Я исследую подобные проекты в качестве натуралиста. Мне интересен генезис новых видов дефектов и недостатков в коде.

Одно из наблюдений – генерированный код более "пухлый", что затрудняет его восприятие человеком и оптимизацию компиляторами. Следующий фрагмент кода как раз это хорошо демонстрирует.
static TensorImpl make_contiguous_tensor(const std::vector<int64_t>& sizes) {
  const std::size_t nd = sizes.size();
  std::vector<int64_t> strides(nd, 0);
  int64_t acc = 1;
  for (std::ptrdiff_t i = static_cast<std::ptrdiff_t>(nd) - 1; i >= 0; --i) {
    strides[static_cast<std::size_t>(i)] = acc;
    const auto sz = sizes[static_cast<std::size_t>(i)];
    acc *= (sz == 0 ? 1 : sz);
  }
 
  int64_t ne = 1;
  bool any_zero = false;
  for (auto s : sizes) {
    if (s == 0) {
      any_zero = true;
      break;
    }
    ne *= s;
  }
  if (any_zero) {
    ne = 0;
  }
  ....
}

С одной стороны, размер и скорость этого кода некритичны, так как он относится к тестам.

Однако этот код размножен по 9 файлам. Оставим за скобками, что такого по-хорошему вообще быть не должно. Но раз код размножается почкованием, желательно чтобы он был бы тогда по возможности компактным.

А ещё важно, что генерируется С++ код, и он обязан быть оптимальным. Само назначение языка – высокоэффективные приложения. В рассматриваемом месте – это просто медленный тест. Но в другом месте что-то подобное приведёт к существенному замедлению приложения. Или скажется кумулятивный эффект множества неудачных фрагментов сгенерированного кода.

Медленный С++ код – это противоестественно. Поэтому по-прежнему полезно развивать свою экспертность в понимании, удачным ли получился код и как его можно улучшить, сократить, оптимизировать. Или сгенерировать снова, используя уточнения.

В общем давайте потренируемся и проведём рефакторинг. Начнём с этого фрагмента:
int64_t ne = 1;
bool any_zero = false;
for (auto s : sizes) {
  if (s == 0) {
    any_zero = true;
    break;
  }
  ne *= s;
}
if (any_zero) {
  ne = 0;
}

Здесь перемножаются все элементы массива. Если встретится 0, то цикл прервётся, чтобы зря не обрабатывать оставшиеся элементы массива. Всё равно ведь ноль получится.

Чтобы обнулить переменную, где хранится произведение, используется флаг any_zero. Этот подход избыточен, можно проще.
int64_t ne = 1;
for (auto s : sizes) {
  if (s == 0) {
    ne = 0;
    break;
  }
  ne *= s;
}

Когда встретится 0, обнулятся ne и цикл завершится. Можно продолжить упрощение. Современные процессоры быстро выполняют операции умножения. Поэтому можно в начале перемножить, а потом уже проверить.
int64_t ne = 1;
for (auto s : sizes) {
  ne *= s;
  if (s == 0) {
    break;
  }
}

Функциональность кода не изменилась, но он стал короче и, на мой взгляд, даже понятнее.
👌2👍1
Короче и быстрей (часть №2 из 5) – Первый цикл

Теперь вернёмся к началу функции.
static TensorImpl make_contiguous_tensor(const std::vector<int64_t>& sizes) {
  const std::size_t nd = sizes.size();
  std::vector<int64_t> strides(nd, 0);
  int64_t acc = 1;
  for (std::ptrdiff_t i = static_cast<std::ptrdiff_t>(nd) - 1; i >= 0; --i) {
    strides[static_cast<std::size_t>(i)] = acc;
    const auto sz = sizes[static_cast<std::size_t>(i)];
    acc *= (sz == 0 ? 1 : sz);
  }

Из-за static_cast код выглядит тяжеловесным. Первое приведение типа нужно, чтобы какие-то компиляторы/статические анализаторы не ругались на странные арифметические игры.
std::ptrdiff_t i = static_cast<std::ptrdiff_t>(nd) - 1;

Рассмотрим, что будет, если убрать static_cast, а входной массив окажется пустым:

1. Если массив пуст, то nd = 0;
2. Вычтя единицу из беззнакового нуля, мы получим SIZE_MAX, т.е. очень большое положительное беззнаковое число.
3. Значение SIZE_MAX типа size_t неявно преобразуется в тип ptrdiff_t и записывается в переменную i. Получается, что i = -1, как и было задумано. Но вот тут как раз могут быть выданы предупреждения. Ведь мы инициализируем ptrdiff_t числом, которое больше диапазона максимально вмещаемого числа. Поведение в такой ситуации до C++20 определяется реализацией – implementation defined behavior. После C++20 поведение определено.

Итого: первый static_cast лучше оставить на месте. Про остальные такого сказать нельзя. В них нет никакого смысла.

Оператор [] в классе vector принимает аргумент типа size_type (этот тип является синонимом size_t). Значение переменной i автоматически будет преобразовано в size_t, и в этом нет чего-то странного, опасного или подозрительного. Явное приведение типов только загромождает код, и от него лучше избавиться.

for (std::ptrdiff_t i = static_cast<std::ptrdiff_t>(nd) - 1; i >= 0; --i) {
  strides[i] = acc;
  const auto sz = sizes[i];
  acc *= (sz == 0 ? 1 : sz);
}

Первый шаг сделан. Можно теперь всё-таки упростить длинную строку с циклом? Давайте подумаем. Хочется написать как-то так:
for (auto sz : std::ranges::views::reverse(sizes)) {
  strides[???? i ????] = acc;
  acc *= (sz == 0 ? 1 : sz);
}

Всё равно требуется переменная i для обхода массива strides, начиная с конца. Так что совсем упростить код и избавиться от i не получается. Поэтому сделаем так:
int64_t acc = 1;
size_t i = nd;
for (auto sz : std::ranges::views::reverse(sizes)) {
  strides[--i] = acc;
  acc *= (sz == 0 ? 1 : sz);
}

Если честно, мне не нравится, что из-за использования --i код стал сложнее. Теперь требуется вникнуть, почему переменная в начале уменьшается, а уже затем используется для обращения к элементу массива.

С другой стороны, кода стало меньше и его можно быстрее просмотреть глазами. Так что, наверное, когнитивная сложность кода осталась в итоге такой же. Т.е. понимать код стало не проще, но и не сложнее. В любом случае код стал покороче, так что ok.
👍1
Короче и быстрей (часть №3 из 5) – Объединяем циклы

Взглянем целиком на вариант кода, который получился к текущему моменту.
static TensorImpl make_contiguous_tensor(const std::vector<int64_t>& sizes) {
  const std::size_t nd = sizes.size();
  std::vector<int64_t> strides(nd, 0);
  int64_t acc = 1;
  size_t i = nd;
  for (auto sz : std::ranges::views::reverse(sizes)) {
    strides[--i] = acc;
    acc *= (sz == 0 ? 1 : sz);
  }
   int64_t ne = 1;
 
  for (auto s : sizes) {
    ne *= s;
    if (ns == 0) {
      break;
    }
  }
  ....
}

Теперь, когда кода меньше, становится очевидным, что второй цикл избыточен. В первом цикле мы перебираем все элементы. Так почему бы их сразу не перемножить?
static TensorImpl make_contiguous_tensor(const std::vector<int64_t>& sizes) {
  const std::size_t nd = sizes.size();
  std::vector<int64_t> strides(nd, 0);
  int64_t acc = 1;
  size_t i = nd;
  int64_t ne = 1;
  for (auto sz : std::ranges::views::reverse(sizes)) {
    strides[--i] = acc;
    acc *= (sz == 0 ? 1 : sz);
    ne *= sz;
  }
  ....
}

Красота. Мы перемножаем все элементы, несмотря на то, что один из них может оказаться нулевым? Нестрашно. Микропроцессоры сейчас быстро умножают. Можно потерять больше на повторном доступе ко всем элементам во втором цикле.

Что ещё осталось? Не требуется изначально обнулять контейнер strides нулями. Всё равно все его элементы будут перезаписаны.
std::vector<int64_t> strides(nd, 0); // надо убрать второй аргумент

В принципе, мы закончили. Но можно сделать ещё одно косметическое изменение, избавившись от переменной nd. Она ни здесь, ни в последующем коде не нужна. Итоговый код:
static TensorImpl make_contiguous_tensor(const std::vector<int64_t>& sizes) {
  auto q = sizes.size();
  std::vector<int64_t> strides(q);
  int64_t acc = 1;
  int64_t ne = 1;
  for (auto sz : std::ranges::views::reverse(sizes)) {
    strides[--q] = acc;
    acc *= (sz == 0 ? 1 : sz);
    ne *= sz;
  }
  ....
}

Код сократился в два раза: с 20 до 9 строк!

P.S. Если захотите и напишите комментарий, я приведу ассемблерный код, чтобы показать как он сократился и оптимизировался.
👍3