Русский
Русский
English
Статистика
Реклама

Блог компании pvs-studio

Обработка дат притягивает ошибки или 77 дефектов в Qt 6

16.02.2021 22:10:00 | Автор: admin

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


Это классическая статья о проверке открытого проекта, которая пополнит нашу "доказательную базу" полезности и эффективности использования PVS-Studio для контроля качества кода. Хотя мы уже писали про поверку проекта Qt (в 2011, в 2014 и в 2018), очень полезно сделать это вновь. Так, мы на практике демонстрируем простую, но очень важную мысль: статический анализ должен применяться регулярно!


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


Всё, теория заканчивается. Давайте посмотрим, что интересного нас ждёт в коде. А пока вы будете читать статью, предлагаю скачать PVS-Studio и запросить демонстрационный ключ, чтобы посмотреть, что интересного найдётся в ваших собственных проектах :).


Даты


Кажется, намечается ещё один паттерн кода, где любят собираться ошибки. Он, конечно, не такой масштабный, как функции сравнения или последние строки в однотипных блоках. Речь идёт про фрагменты кода, работающие с датами. Наверное, такой код сложно тестировать, и в итоге эти недотестированные функции будут давать некорректный результат на определённых наборах входных данных. Про пару таких случаев уже было рассказано в статье "Почему PVS-Studio не предлагает автоматические правки кода".


Встретились ошибки обработки дат и в Qt. Давайте с них и начнём.


Фрагмент N1: неправильная интерпретация статуса ошибки


Для начала нам следует посмотреть, как устроена функция, возвращающая номер месяца по его сокращённому названию.


static const char qt_shortMonthNames[][4] = {    "Jan", "Feb", "Mar", "Apr", "May", "Jun",    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};static int fromShortMonthName(QStringView monthName){  for (unsigned int i = 0;       i < sizeof(qt_shortMonthNames) / sizeof(qt_shortMonthNames[0]); ++i)  {    if (monthName == QLatin1String(qt_shortMonthNames[i], 3))      return i + 1;  }  return -1;}

В случае успеха функция возвращает номер месяца (значение от 1 до 12). Если имя месяца некорректно, то функция возвращает отрицательное значение (-1). Обратите внимание, что функция не может вернуть значение 0.


Но там, где только что рассмотренная функция используется, программист как раз рассчитывает, что в качестве статуса ошибки ему будет возвращено нулевое значение. Фрагмент кода, некорректно использующий функцию fromShortMonthName:


QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format){  ....  month = fromShortMonthName(parts.at(1));  if (month)    day = parts.at(2).toInt(&ok);  // If failed, try day then month  if (!ok || !month || !day) {    month = fromShortMonthName(parts.at(2));    if (month) {      QStringView dayPart = parts.at(1);      if (dayPart.endsWith(u'.'))        day = dayPart.chopped(1).toInt(&ok);    }  }  ....}

Проверки номера месяца на равенство нулю никогда не сработают, и программа продолжит выполняться с некорректным отрицательным номером месяца. Анализатор PVS-Studio видит здесь целый букет несостыковок, о чем сообщает сразу четырьмя сообщениями:


  • V547 [CWE-571] Expression 'month' is always true. qdatetime.cpp 4907
  • V560 [CWE-570] A part of conditional expression is always false: !month. qdatetime.cpp 4911
  • V547 [CWE-571] Expression 'month' is always true. qdatetime.cpp 4913
  • V560 [CWE-570] A part of conditional expression is always false: !month. qdatetime.cpp 4921

Фрагмент N2: ошибка логики обработки даты


Для начала посмотрим на реализацию функции, возвращающей количество секунд.


enum {  ....  MSECS_PER_DAY = 86400000,  ....  SECS_PER_MIN = 60,};int QTime::second() const{    if (!isValid())        return -1;    return (ds() / 1000)%SECS_PER_MIN;}

Рассмотренная функция может вернуть значение в диапазоне [0..59] или статус ошибки -1.


В одном месте эта функция используется очень странным образом:


static qint64 qt_mktime(QDate *date, QTime *time, ....){  ....  } else if (yy == 1969 && mm == 12 && dd == 31             && time->second() == MSECS_PER_DAY - 1) {      // There was, of course, a last second in 1969, at time_t(-1); we won't      // rescue it if it's not in normalised form, and we don't know its DST      // status (unless we did already), but let's not wantonly declare it      // invalid.  } else {  ....}

Предупреждение PVS-Studio: V560 [CWE-570] A part of conditional expression is always false: time->second() == MSECS_PER_DAY 1. qdatetime.cpp 2488


Согласно комментарию, если что-то пошло не так, то лучше ничего не делать. Однако, условие всегда будет ложным, поэтому всегда выполняется else-ветка.


Ошибочно вот это сравнение:


time->second() == MSECS_PER_DAY - 1

MSECS_PER_DAY 1 это 86399999. Функция second, как мы уже знаем, никак не может вернуть такое значение. Таким образом, здесь какая-то логическая ошибка и код заслуживает пристального внимания разработчиков.


Сильная сторона статических анализаторов кода в том, что они проверяют все ситуации, независимо от частоты их выполнения. Таким образом, статический анализ хорошо дополняет юнит-тесты и другие методы контроля качества кода.


Опечатки


Фрагмент N3: неожиданно, мы поговорим о HTML!


QString QPixelTool::aboutText() const{  const QList<QScreen *> screens = QGuiApplication::screens();  const QScreen *windowScreen = windowHandle()->screen();  QString result;  QTextStream str(&result);  str << "<html></head><body><h2>Qt Pixeltool</h2><p>Qt " << QT_VERSION_STR    << "</p><p>Copyright (C) 2017 The Qt Company Ltd.</p><h3>Screens</h3><ul>";  for (const QScreen *screen : screens)    str << "<li>" << (screen == windowScreen ? "* " : "  ")        << screen << "</li>";  str << "<ul></body></html>";  return result;}

Предупреждение PVS-Studio: V735 Possibly an incorrect HTML. The "</ body>" closing tag was encountered, while the "</ ul>" tag was expected. qpixeltool.cpp 707


В PVS-Studio есть диагностики, которые не только проверят сам код, но и выискивают аномалии в строковых константах. Здесь как раз сработала одна из таких диагностик. Это достаточно редкий случай, зато этим он и примечательный.


Дважды используется тег открытия списка. Это явная опечатка. Первый тег, должен открывать список, а второй закрывать. Правильный код:


str << "</ul></body></html>";

Фрагмент N4: повторная проверка в условии


class Node{  ....  bool isGroup() const { return m_nodeType == Group; }  ....};void DocBookGenerator::generateDocBookSynopsis(const Node *node){  ....  if (node->isGroup() || node->isGroup()      || node->isSharedCommentNode() || node->isModule()      || node->isJsModule() || node->isQmlModule() || node->isPageNode())    return;  ....}

Предупреждение PVS-Studio: V501 [CWE-570] There are identical sub-expressions to the left and to the right of the '||' operator: node->isGroup() || node->isGroup() docbookgenerator.cpp 2599


Простая опечатка, но её исправление зависит от того, чего на самом деле хотели достичь в этом коде. Если проверка просто дублируется, то её стоит просто удалить. Но возможен и другой сценарий: не проверено какое-то другое нужное условие.


Фрагмент N5: создание лишней локальной переменной


void MainWindow::addToPhraseBook(){  ....  QString selectedPhraseBook;  if (phraseBookList.size() == 1) {    selectedPhraseBook = phraseBookList.at(0);    if (QMessageBox::information(this, tr("Add to phrase book"),          tr("Adding entry to phrasebook %1").arg(selectedPhraseBook),           QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok)                          != QMessageBox::Ok)      return;  } else {    bool okPressed = false;    QString selectedPhraseBook =       QInputDialog::getItem(this, tr("Add to phrase book"),                            tr("Select phrase book to add to"),                            phraseBookList, 0, false, &okPressed);    if (!okPressed)      return;  }  MessageItem *currentMessage = m_dataModel->messageItem(m_currentIndex);  Phrase *phrase = new Phrase(currentMessage->text(),                              currentMessage->translation(),                              QString(), nullptr);  phraseBookHash.value(selectedPhraseBook)->append(phrase);}

Кстати, если хотите, можете проверить свою внимательность и самостоятельно поискать ошибку. А чтобы вы сразу случайно не увидели ответ, добавлю сюда единорожку из старой коллекции. Возможно, вы даже его ещё не видели :).


Единорог из старой коллекции


Предупреждение PVS-Studio: V561 [CWE-563] It's probably better to assign value to 'selectedPhraseBook' variable than to declare it anew. Previous declaration: mainwindow.cpp, line 1303. mainwindow.cpp 1313


В обоих ветках условного оператора формируется текст, который должен быть записан в переменную selectedPhraseBook. Имя переменной длинное, и программисту было лень писать его заново. Поэтому он скопировал имя переменной из того места, где она объявляется. При этом он чуть поспешил и вместе с именем скопировал и тип переменной:


QString selectedPhraseBook =

В результате в else-блоке появилась ещё одна локальная строковая переменная, которая инициализируется, но не используется. Тем временем переменная с таким же именем во внешнем блоке останется пустой.


Фрагмент N6: приоритет операций


Классический паттерн ошибки, который встречается весьма часто.


bool QQmlImportInstance::resolveType(....){  ....  if (int icID = containingType.lookupInlineComponentIdByName(typeStr) != -1)  {    *type_return = containingType.lookupInlineComponentById(icID);  } else {    auto icType = createICType();    ....  }  ....}

Предупреждение PVS-Studio: V593 [CWE-783] Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. qqmlimport.cpp 754


Значение переменной icID всегда будет иметь значение 0 или 1. Это явно не то, что задумывалось. Причина: в начале происходит сравнение с -1, а только затем инициализация переменной icID.


Современный синтаксис C++ позволяет корректно записать это условие следующим образом:


if (int icID = containingType.lookupInlineComponentIdByName(typeStr);    icID != -1)

Кстати, очень похожую ошибку мы уже обнаруживали в Qt:


char ch;while (i < dataLen && ((ch = data.at(i) != '\n') && ch != '\r'))  ++i;

Но пока на вооружение не будет взят такой анализатор кода, как PVS-Studio, программисты вновь и вновь будут допускать такие ошибки. Никто не совершенен. И да, это тонкий намёк внедрить PVS-Studio :).


Фрагмент N7: коварное деление по модулю


Часто бывает необходимо определить, делится число без остатка на 2 или нет. Правильный вариант это поделить по модулю два и проверить результат:


if (A % 2 == 1)

Но программисты вновь и вновь ошибаются и пишут что-то типа этого:


if (A % 1 == 1)

Это естественно неправильно, так как остаток от деления по модулю на один это всегда ноль. Не обошлось без этой ошибки и в Qt:


bool loadQM(Translator &translator, QIODevice &dev, ConversionData &cd){  ....  case Tag_Translation: {    int len = read32(m);    if (len % 1) {                                             // <=      cd.appendError(QLatin1String("QM-Format error"));      return false;    }    m += 4;    QString str = QString((const QChar *)m, len/2);  ....}

Предупреждение PVS-Studio: V1063 The modulo by 1 operation is meaningless. The result will always be zero. qm.cpp 549


Фрагмент N8: перезаписывание значения


QString Node::qualifyQmlName(){  QString qualifiedName = m_name;  if (m_name.startsWith(QLatin1String("QML:")))    qualifiedName = m_name.mid(4);  qualifiedName = logicalModuleName() + "::" + m_name;  return qualifiedName;}

Предупреждение PVS-Studio: V519 [CWE-563] The 'qualifiedName' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1227, 1228. node.cpp 1228


Как я понимаю, случайно использовали не то имя переменной. Предполагаю, здесь должно быть написано так:


QString qualifiedName = m_name;if (m_name.startsWith(QLatin1String("QML:")))  qualifiedName = m_name.mid(4);qualifiedName = logicalModuleName() + "::" + qualifiedName;return qualifiedName;

Фрагмент N9: copy-paste


class Q_CORE_EXPORT QJsonObject{  ....  bool operator<(const iterator& other) const  { Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }  bool operator<=(const iterator& other) const  { Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }  ....}

Прудпреждение PVS-Studio: V524 It is odd that the body of '<=' function is fully equivalent to the body of '<' function. qjsonobject.h 155


Такие скучные функции, как операторы сравнения, никто не проверяет. На них обычно не пишут тесты. Их не просматривают или делают это очень быстро на code review. А зря. Статический анализ кода здесь как нельзя кстати. Анализатор не устаёт и с удовольствием проверяет даже такой скучный код.


Реализация операторов < и <= совпадают. Это явно неправильно. Скорее всего, код писался методом Copy-Paste, и затем забыли изменить всё что нужно в скопированном коде. Правильно:


bool operator<(const iterator& other) const{ Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }bool operator<=(const iterator& other) const{ Q_ASSERT(item.o == other.item.o); return item.index <= other.item.index; }

Фрагмент N10: static_cast / dynamic_cast


void QSGSoftwareRenderThread::syncAndRender(){  ....  bool canRender = wd->renderer != nullptr;  if (canRender) {     auto softwareRenderer = static_cast<QSGSoftwareRenderer*>(wd->renderer);     if (softwareRenderer)       softwareRenderer->setBackingStore(backingStore);  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'softwareRenderer' is always true. qsgsoftwarethreadedrenderloop.cpp 510


В начале рассмотрим вот эту проверку:


bool canRender = wd->renderer != nullptr;if (canRender) {

Благодаря ей можно быть уверенным, что внутри тела условного оператора значение указателя wd->renderer всегда точно ненулевое. Тогда непонятно, что же хотят проверить следующим кодом?


auto softwareRenderer = static_cast<QSGSoftwareRenderer*>(wd->renderer);if (softwareRenderer)

Если указатель wd->renderer ненулевой, то и указатель softwareRenderer точно ненулевой. Есть подозрение, что здесь опечатка, которая состоит в том, что на самом деле следовало использовать dynamic_cast. В этом случае код начинает приобретать смысл. Если преобразование типа невозможно, оператор dynamic_cast возвращает nullptr, и это возвращенное значение, естественно, следует проверять. Впрочем, возможно, я неправильно интерпретировал ситуацию и код нужно исправлять другим способом.


Фрагмент N11: скопировали блок кода и забыли изменить


void *QQuickPath::qt_metacast(const char *_clname){  if (!_clname) return nullptr;  if (!strcmp(_clname, qt_meta_stringdata_QQuickPath.stringdata0))    return static_cast<void*>(this);  if (!strcmp(_clname, "QQmlParserStatus"))    return static_cast< QQmlParserStatus*>(this);  if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))   // <=    return static_cast< QQmlParserStatus*>(this);  if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))   // <=    return static_cast< QQmlParserStatus*>(this);  return QObject::qt_metacast(_clname);}

Предупреждение PVS-Studio: V581 [CWE-670] The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 2719, 2721. moc_qquickpath_p.cpp 2721


Эти две строчки:


if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))  return static_cast< QQmlParserStatus*>(this);

Были размножены с помощью Copy-Paste. После чего они не были модифицированы и не имеют смысла.


Фрагмент N12: переполнение из-за не там поставленной скобки


int m_offsetFromUtc;....void QDateTime::setMSecsSinceEpoch(qint64 msecs){  ....  if (!add_overflow(msecs, qint64(d->m_offsetFromUtc * 1000), &msecs))    status |= QDateTimePrivate::ValidWhenMask;  ....}

Предупреждение PVS-Studio: V1028 [CWE-190] Possible overflow. Consider casting operands of the 'd->m_offsetFromUtc * 1000' operator to the 'qint64' type, not the result. qdatetime.cpp 3922


Программист предвидит ситуацию, что при умножении переменной типа int на 1000 может произойти переполнение. Чтобы этого избежать, он планирует использовать при умножении 64-битный тип qint64. И использует явное приведение типа.


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


add_overflow(msecs, qint64(d->m_offsetFromUtc) * 1000, &msecs)

Фрагмент N13: не полностью инициализированный массив


class QPathEdge{  ....private:  int m_next[2][2];  ....};inline QPathEdge::QPathEdge(int a, int b)    : flag(0)    , windingA(0)    , windingB(0)    , first(a)    , second(b)    , angle(0)    , invAngle(0){    m_next[0][0] = -1;    m_next[1][0] = -1;    m_next[0][0] = -1;    m_next[1][0] = -1;}

Предупреждения PVS-Studio:


  • V1048 [CWE-1164] The 'm_next[0][0]' variable was assigned the same value. qpathclipper_p.h 301
  • V1048 [CWE-1164] The 'm_next[1][0]' variable was assigned the same value. qpathclipper_p.h 302

Перед нами неудачная попытка инициализировать массив размером 2x2. Два элемента инициализируются повторно, и два остаются неинициализированными. Правильный вариант:


m_next[0][0] = -1;m_next[0][1] = -1;m_next[1][0] = -1;m_next[1][1] = -1;

Я очень люблю такие примеры ошибок, которые встречаются в коде профессиональных разработчиков. Это как раз такой случай. Он показывает, что любой может опечататься, и поэтому статический анализ является вашим другом. Дело в том, что я уже десяток лет веду бой со скептиками, которые уверены, что такие ошибки можно встретить только в лабораторных работах студентов и что они такие ошибки никогда не делают :). Ещё 10 лет назад я написал заметку "Миф второй профессиональные разработчики не допускают глупых ошибок", и с тех пор, естественно, ничего не изменилось. Люди всё также делают такие ошибки и всё также утверждают, что это не так :).


Будь мудр - используй статический анализатор кода


Ошибки в логике


Фрагмент N14: недостижимый код


void QmlProfilerApplication::tryToConnect(){  Q_ASSERT(!m_connection->isConnected());  ++ m_connectionAttempts;  if (!m_verbose && !(m_connectionAttempts % 5)) {// print every 5 seconds    if (m_verbose) {      if (m_socketFile.isEmpty())        logError(          QString::fromLatin1("Could not connect to %1:%2 for %3 seconds ...")          .arg(m_hostName).arg(m_port).arg(m_connectionAttempts));      else        logError(          QString::fromLatin1("No connection received on %1 for %2 seconds ...")          .arg(m_socketFile).arg(m_connectionAttempts));    }  }  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'm_verbose' is always false. qmlprofilerapplication.cpp 495


Этот код никогда ничего не запишет в лог. Причиной являются противоположные условия:


if (!m_verbose && ....) {  if (m_verbose) {

Фрагмент N15: перетирание значения переменной


void QRollEffect::scroll(){  ....  if (currentHeight != totalHeight) {      currentHeight = totalHeight * (elapsed/duration)          + (2 * totalHeight * (elapsed%duration) + duration)          / (2 * duration);      // equiv. to int((totalHeight*elapsed) / duration + 0.5)      done = (currentHeight >= totalHeight);  }  done = (currentHeight >= totalHeight) &&         (currentWidth >= totalWidth);  ....}

Предупреждение PVS-Studio: V519 [CWE-563] The 'done' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 509, 511. qeffects.cpp 511


Весь условный оператор не имеет смысла, так как значение переменной done всё равно тут же перезаписывается. Возможно, здесь не хватает ключевого слова else.


Фрагмент N16-N20: перетирание значения переменной


Альтернативный вариант перетирания значения переменной.


bool QXmlStreamWriterPrivate::finishStartElement(bool contents){  ....  if (inEmptyElement) {    ....    lastNamespaceDeclaration = tag.namespaceDeclarationsSize;   // <=    lastWasStartElement = false;  } else {    write(">");  }  inStartElement = inEmptyElement = false;  lastNamespaceDeclaration = namespaceDeclarations.size();      // <=  return hadSomethingWritten;}

Предупреждение PVS-Studio: V519 [CWE-563] The 'lastNamespaceDeclaration' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 3030, 3036. qxmlstream.cpp 3036


Возможно, первая запись значения в переменную lastNamespaceDeclaration является лишней и её можно удалить. А возможно, перед нами серьезная логическая ошибка.


Есть ещё 4 предупреждения, указывающих на такой же паттерн ошибки:


  • V519 [CWE-563] The 'last' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 609, 637. qtextengine.cpp 637
  • V519 [CWE-563] The 'm_dirty' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1014, 1017. qquickshadereffect.cpp 1017
  • V519 [CWE-563] The 'changed' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 122, 128. qsgdefaultspritenode.cpp 128
  • V519 [CWE-563] The 'eaten' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 299, 301. qdesigner.cpp 301

Фрагмент N21: путаница между нулевым указателем и пустой строкой


// this could become a list of all languages used for each writing// system, instead of using the single most common language.static const char languageForWritingSystem[][6] = {    "",     // Any    "en",  // Latin    "el",  // Greek    "ru",  // Cyrillic    ...... // Нулевых указателей нет. Используются пустые строковые литералы.    "", // Symbol    "sga", // Ogham    "non", // Runic    "man" // N'Ko};static void populateFromPattern(....){  ....  for (int j = 1; j < QFontDatabase::WritingSystemsCount; ++j) {    const FcChar8 *lang = (const FcChar8*) languageForWritingSystem[j];    if (lang) {  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'lang' is always true. qfontconfigdatabase.cpp 462


В массиве languageForWritingSystem нет нулевых указателей. Поэтому проверка if(lang) не имеет смысла. Зато в массиве есть пустые строки. Быть может, хотелось сделать проверку именно на пустую строку? Если да, тогда корректный код должен выглядеть так:


if (strlen(lang) != 0) {

Или можно ещё проще написать:


if (lang[0] != '\0') {

Фрагмент N22: странная проверка


bool QNativeSocketEnginePrivate::createNewSocket(....){  ....  int socket = qt_safe_socket(domain, type, protocol, O_NONBLOCK);  ....  if (socket < 0) {    ....    return false;  }  socketDescriptor = socket;  if (socket != -1) {    this->socketProtocol = socketProtocol;    this->socketType = socketType;  }  return true;}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'socket != 1' is always true. qnativesocketengine_unix.cpp 315


Условие socket != -1 всегда истинно, так как выше происходит выход из функции, если значение переменной socket отрицательное.


Фрагмент N23: так что же всё-таки должна вернуть функция?


bool QSqlTableModel::removeRows(int row, int count, const QModelIndex &parent){  Q_D(QSqlTableModel);  if (parent.isValid() || row < 0 || count <= 0)    return false;  else if (row + count > rowCount())    return false;  else if (!count)    return true;  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression '!count' is always false. qsqltablemodel.cpp 1110


Для упрощения выделю самое главное:


if (.... || count <= 0)  return false;....else if (!count)  return true;

Первая проверка говорит нам, что если значение count меньше или равно 0, то это ошибочное состояние и функция должна вернуть false. Однако ниже мы видим точное сравнение этой переменной с нулём, и этот случай уже интерпретируется по-другому: функция должна вернуть true.


Здесь явно что-то не так. Я подозреваю, что на самом деле проверка должна быть не <=, а просто <. Тогда код обретает смысл:


bool QSqlTableModel::removeRows(int row, int count, const QModelIndex &parent){  Q_D(QSqlTableModel);  if (parent.isValid() || row < 0 || count < 0)    return false;  else if (row + count > rowCount())    return false;  else if (!count)    return true;  ....}

Фрагмент N24: лишний статус?


В следующем коде переменная identifierWithEscapeChars выглядит просто как лишняя сущность. Или это логическая ошибка? Или код не дописан? К моменту второй проверки эта переменная в любом случае всегда будет равна true.


int Lexer::scanToken(){  ....  bool identifierWithEscapeChars = false;  ....  if (!identifierWithEscapeChars) {    identifierWithEscapeChars = true;    ....  }  ....  if (identifierWithEscapeChars) {    // <=    ....  }  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'identifierWithEscapeChars' is always true. qqmljslexer.cpp 817


Фрагмент N25: что делать с девятью объектами?


bool QFont::fromString(const QString &descrip){  ....  const int count = l.count();  if (!count || (count > 2 && count < 9) || count == 9 || count > 17 ||      l.first().isEmpty()) {    qWarning("QFont::fromString: Invalid description '%s'",             descrip.isEmpty() ? "(empty)" : descrip.toLatin1().data());    return false;  }  setFamily(l[0].toString());  if (count > 1 && l[1].toDouble() > 0.0)    setPointSizeF(l[1].toDouble());  if (count == 9) {                           // <=    setStyleHint((StyleHint) l[2].toInt());    setWeight(QFont::Weight(l[3].toInt()));    setItalic(l[4].toInt());    setUnderline(l[5].toInt());    setStrikeOut(l[6].toInt());    setFixedPitch(l[7].toInt());  } else if (count >= 10) {  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'count == 9' is always false. qfont.cpp 2142


Как должна себя вести функция, если переменная count равна 9? С одной стороны, функция должна выдать предупреждение и завершить свою работу. Ведь явно написано:


if (.... || count == 9 || ....) {  qWarning(....);  return false;}

С другой стороны, для 9 объектов предусмотрено выполнение специального кода:


if (count == 9) {  setStyleHint((StyleHint) l[2].toInt());  setWeight(QFont::Weight(l[3].toInt()));  setItalic(l[4].toInt());  ....}

Этот код, конечно, никогда не выполняется. Код ждёт, чтобы его пришли и исправили :).


Нулевые указатели


Фрагмент N26-N42: использование указателя до его проверки


class __attribute__((visibility("default"))) QMetaType {  ....  const QtPrivate::QMetaTypeInterface *d_ptr = nullptr;};QPartialOrdering QMetaType::compare(const void *lhs, const void *rhs) const{    if (!lhs || !rhs)        return QPartialOrdering::Unordered;    if (d_ptr->flags & QMetaType::IsPointer)        return threeWayCompare(*reinterpret_cast<const void * const *>(lhs),                               *reinterpret_cast<const void * const *>(rhs));    if (d_ptr && d_ptr->lessThan) {        if (d_ptr->equals && d_ptr->equals(d_ptr, lhs, rhs))            return QPartialOrdering::Equivalent;        if (d_ptr->lessThan(d_ptr, lhs, rhs))            return QPartialOrdering::Less;        if (d_ptr->lessThan(d_ptr, rhs, lhs))            return QPartialOrdering::Greater;        if (!d_ptr->equals)            return QPartialOrdering::Equivalent;    }    return QPartialOrdering::Unordered;}

Предупреждение PVS-Studio: V595 [CWE-476] The 'd_ptr' pointer was utilized before it was verified against nullptr. Check lines: 710, 713. qmetatype.cpp 710


Ошибка на первый взгляд может быть не заметна. Но на самом деле всё просто. Проследим, как работают с указателем d_ptr:


if (d_ptr->flags & ....)if (d_ptr && ....)

Вначале выполняется условие, в котором указатель разыменовывается. И только из следующей проверки становится ясно, что этот указатель, оказывается, может быть нулевой.


Это один из самых распространённых паттернов ошибки в языке C и С++. Пруфы. В исходных Qt кодах тоже встречается немало ошибок этой разновидности:


  • V595 [CWE-476] The 'self' pointer was utilized before it was verified against nullptr. Check lines: 1346, 1351. qcoreapplication.cpp 1346
  • V595 [CWE-476] The 'currentTimerInfo' pointer was utilized before it was verified against nullptr. Check lines: 636, 641. qtimerinfo_unix.cpp 636
  • V595 [CWE-476] The 'lib' pointer was utilized before it was verified against nullptr. Check lines: 325, 333. qlibrary.cpp 325
  • V595 [CWE-476] The 'fragment.d' pointer was utilized before it was verified against nullptr. Check lines: 2262, 2266. qtextcursor.cpp 2262
  • V595 [CWE-476] The 'window' pointer was utilized before it was verified against nullptr. Check lines: 1581, 1583. qapplication.cpp 1581
  • V595 [CWE-476] The 'window' pointer was utilized before it was verified against nullptr. Check lines: 1593, 1595. qapplication.cpp 1593
  • V595 [CWE-476] The 'newHandle' pointer was utilized before it was verified against nullptr. Check lines: 873, 879. qsplitter.cpp 873
  • V595 [CWE-476] The 'targetModel' pointer was utilized before it was verified against nullptr. Check lines: 454, 455. qqmllistmodel.cpp 454
  • V595 [CWE-476] The 'childIface' pointer was utilized before it was verified against nullptr. Check lines: 102, 104. qaccessiblequickitem.cpp 102
  • V595 [CWE-476] The 'e' pointer was utilized before it was verified against nullptr. Check lines: 94, 98. qquickwindowmodule.cpp 94
  • V595 [CWE-476] The 'm_texture' pointer was utilized before it was verified against nullptr. Check lines: 235, 239. qsgplaintexture.cpp 235
  • V595 [CWE-476] The 'm_unreferencedPixmaps' pointer was utilized before it was verified against nullptr. Check lines: 1140, 1148. qquickpixmapcache.cpp 1140
  • V595 [CWE-476] The 'camera' pointer was utilized before it was verified against nullptr. Check lines: 263, 264. assimpimporter.cpp 263
  • V595 [CWE-476] The 'light' pointer was utilized before it was verified against nullptr. Check lines: 273, 274. assimpimporter.cpp 273
  • V595 [CWE-476] The 'channel' pointer was utilized before it was verified against nullptr. Check lines: 337, 338. assimpimporter.cpp 337
  • V595 [CWE-476] The 'm_fwb' pointer was utilized before it was verified against nullptr. Check lines: 2492, 2500. designerpropertymanager.cpp 2492

Фрагмент N43: использование указателя до его проверки в рамках одного выражения


Та же самая ситуация, что и выше. Однако в этот раз разыменование и проверка указателя находятся в одном выражении. Классическая ошибка, возникающая из-за невнимательности при написании кода и при последующем code-review.


void QFormLayoutPrivate::updateSizes(){  ....  QFormLayoutItem *field = m_matrix(i, 1);  ....  if (userHSpacing < 0 && !wrapAllRows && (label || !field->fullRow) && field)  ....}

Предупреждение PVS-Studio: V713 [CWE-476] The pointer 'field' was utilized in the logical expression before it was verified against nullptr in the same logical expression. qformlayout.cpp 405


Минутка отдыха


Я устал. Думаю, устали и читатели. Тут устанешь, даже если просто бегло просматривать текст статьи :). Поэтому я пошёл за второй чашечкой кофе. Первую я выпил где-то на рубеже 12-ого фрагмента. Приглашаю и читателей сходить за любимым напитком.


И пока все ушли на кухню, рекламная пауза. Приглашаю команду, занимающуюся разработкой проекта Qt рассмотреть вопрос приобретения лицензии на анализатор кода PVS-Studio. Запросить прайс можно здесь. С нашей стороны поддержка и помощь в настройке. Да, согласен, я сегодня более навязчив, чем обычно. Это эксперимент :).


Минутка отдыха с единорогом


Фрагмент N44-N72: нет проверки, что вернула функция malloc


void assignData(const QQmlProfilerEvent &other){  if (m_dataType & External) {    uint length = m_dataLength * (other.m_dataType / 8);    m_data.external = malloc(length);                          // <=    memcpy(m_data.external, other.m_data.external, length);    // <=  } else {    memcpy(&m_data, &other.m_data, sizeof(m_data));  }}

Предупреждение PVS-Studio: V575 [CWE-628] The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 277, 276. qqmlprofilerevent_p.h 277


Нельзя просто взять и использовать указатель, который вернула функция malloc. Нужно обязательно проверить, не является ли этот указатель нулевым, даже если очень лень всем этим заниматься. Для этого есть 4 причины, которые описаны в статье "Почему важно проверять, что вернула функция malloc".


Перед нами один из случаев, где отсутствует необходимая проверка. Есть и другие предупреждения, но из-за их количества включать весь список в статью не хочется. На всякий случай, я выписал 28 предупреждений в файл: qt6-malloc.txt. Но на самом деле разработчикам, конечно, лучше самим перепроверить проект и самостоятельно изучить предупреждения. У меня не было задачи выявить как можно больше ошибок.


Что интересно, на фоне важных забытых проверок есть совершенно ненужные. Речь идёт о вызове оператора new, который в случае ошибки выделения памяти сгенерирует исключение std::bad_alloc. Вот один из примеров такой избыточной проверки:


static QImageScaleInfo* QImageScale::qimageCalcScaleInfo(....){  ....  QImageScaleInfo *isi;  ....  isi = new QImageScaleInfo;  if (!isi)    return nullptr;  ....}

Предупреждение PVS-Studio: V668 [CWE-570] There is no sense in testing the 'isi' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. qimagescale.cpp 245


P.S. Здесь читатели всегда задают вопрос, учитывает ли анализатор placement new или "new (std::nothrow) T"? Да, учитывает и не выдаёт для них ложные срабатывания.


Избыточный код ("код с запахом")


Встречаются ситуации, когда анализатор выдаёт предупреждения на код, который является корректным, но избыточным. Например, есть повторная проверка одной и той же переменной. При этом даже затруднительно сказать, ложное это срабатывание или нет. Формально анализатор прав, но и настоящую ошибку он не нашёл.


Пожалуй, можно сказать, что это "код с запахом". Раз этот код смущает анализатор, он может смущать и программистов, усложняя поддержку такого кода. Приходится тратить больше времени, чтобы понять, что происходит.


Часто я вообще не рассматриваю в статьях такие предупреждения. Это неинтересно. Однако в проекте Qt вдруг таких случаев оказалось удивительно много. Явно больше, чем обычно. Поэтому я решил уделить этому немного внимания и разобрать несколько таких случаев. Думаю, будет полезно провести рефакторинг этих и многих других аналогичных мест. Для этого потребуется использовать полный отчёт, не руководствоваться тем, что я выпишу в статью.


Итак, взглянем на несколько показательных случаев.


Фрагмент N73: "код с запахом" обратная проверка


void QQuick3DSceneManager::setWindow(QQuickWindow *window){  if (window == m_window)    return;  if (window != m_window) {    if (m_window)      disconnect(....);    m_window = window;    connect(....);    emit windowChanged();  }}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'window != m_window' is always true. qquick3dscenemanager.cpp 60


Если window==m_window, то функция завершает работу. Последующая обратная проверка не имеет никакого смысла и только загромождает код.


Фрагмент N74: "код с запахом" странная инициализация


QModelIndex QTreeView::moveCursor(....){  ....  int vi = -1;  if (vi < 0)    vi = qMax(0, d->viewIndex(current));  ....}

Предупреждение PVS-Stduio: V547 [CWE-571] Expression 'vi < 0' is always true. qtreeview.cpp 2219


Что это? Зачем так писать?


Что это? Зачем так писать? Код можно упростить до одной строки:


int vi = qMax(0, d->viewIndex(current));

Фрагмент N75: "код с запахом" недостижимый код


bool copyQtFiles(Options *options){  ....  if (unmetDependencies.isEmpty()) {    if (options->verbose) {      fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",              qPrintable(sourceFileName));    }  } else {    if (unmetDependencies.isEmpty()) {      if (options->verbose) {        fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",                  qPrintable(sourceFileName));      }    } else {      fprintf(stdout, "  -- Skipping %s. It has unmet dependencies: %s.\n",              qPrintable(sourceFileName),              qPrintable(unmetDependencies.join(QLatin1Char(','))));    }  }  ....}

Предупреждение PVS-Studio: V571 [CWE-571] Recurring check. The 'if (unmetDependencies.isEmpty())' condition was already verified in line 2203. main.cpp 2209


На первый взгляд перед нами респектабельный код, формирующий подсказку. Но давайте приглядимся. Если первый раз условие unmetDependencies.isEmpty() выполнилось, то второй раз этого уже не произойдёт. Это нестрашно, так как автор планировал вывести то же самое сообщение. Настоящей ошибки нет, но код переусложнён. Он может быть упрощен до следующего варианта:


bool copyQtFiles(Options *options){  ....  if (unmetDependencies.isEmpty()) {    if (options->verbose) {      fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",              qPrintable(sourceFileName));    }  } else {    fprintf(stdout, "  -- Skipping %s. It has unmet dependencies: %s.\n",            qPrintable(sourceFileName),            qPrintable(unmetDependencies.join(QLatin1Char(','))));  }  ....}

Фрагмент N76: "код с запахом" сложный тернарный оператор


bool QDockAreaLayoutInfo::insertGap(....){  ....  QDockAreaLayoutItem new_item    = widgetItem == nullptr      ? QDockAreaLayoutItem(subinfo)      : widgetItem ? QDockAreaLayoutItem(widgetItem)                    : QDockAreaLayoutItem(placeHolderItem);  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'widgetItem' is always true. qdockarealayout.cpp 1167


Возможно, здесь мы имеем дело с настоящей ошибкой. Но я больше склоняюсь к тому, что этот код несколько раз менялся и в результате получился бессмысленно сложным (избыточным). Его можно сократить до:


  QDockAreaLayoutItem new_item    = widgetItem == nullptr      ? QDockAreaLayoutItem(subinfo) : QDockAreaLayoutItem(widgetItem);

Фрагмент N77: "код с запахом" избыточная защита


typedef unsigned int uint;ReturnedValue TypedArrayCtor::virtualCallAsConstructor(....){  ....  qint64 l = argc ? argv[0].toIndex() : 0;  if (scope.engine->hasException)    return Encode::undefined();  // ### lift UINT_MAX restriction  if (l < 0 || l > UINT_MAX)    return scope.engine->throwRangeError(QLatin1String("Index out of range."));  uint len = (uint)l;  if (l != len)    scope.engine->throwRangeError(      QStringLiteral("Non integer length for typed array."));  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'l != len' is always false. qv4typedarray.cpp 306


Кто-то очень переживает, что значение 64-битной переменной не вмещается в 32-битную переменную unsigned. И использует сразу две проверки корректности. При этом вторая проверка избыточна.


Вот этого условия более чем достаточно:


if (l < 0 || l > UINT_MAX)

Приведенный ниже фрагмент можно смело удалить, и программа менее надёжной не станет:


uint len = (uint)l;if (l != len)  scope.engine->throwRangeError(    QStringLiteral("Non integer length for typed array."));

Дальше продолжать не буду. Думаю, идею вы поняли.


Здесь можно сделать маленький вывод: результатом использования анализатора PVS-Studio будет не только устранение ошибок, но и упрощение кода.


Другие ошибки


Я остановился после того, как описал 77 дефектов. Это красивое число и выписанного более чем достаточно, чтобы написать статью. Однако это не значит, что нет других ошибок, которые способен выявить PVS-Studio. При изучении лога я был весьма поверхностен и пропускал всё, где нужно было разбираться более пары минут, ошибка это или нет :).


Поэтому я всегда прошу не полагаться на ошибки, которые мы описываем в наших статьях, а выполнять анализ проектов самостоятельно.


Заключение


Статический анализ это круто! После внедрения PVS-Studio будет экономить время и нервы, выявляя множество ошибок сразу после написания кода. Намного лучше искать на code review с коллегами не опечатки, а высокоуровневые ошибки и обсуждать эффективность реализованного алгоритма. Тем более, как показывает практика, эти дурацкие опечатки всё равно отлично прячутся при просмотре кода глазами. Так что пусть их лучше ищет программа, а не человек.


Если у вас ещё остались вопросы или возражения, приглашаю познакомиться со статьёй "Причины внедрить в процесс разработки статический анализатор кода PVS-Studio". С вероятность 90 % вы найдете в ней ответ на ваши вопросы :). В оставшихся 10 % случаев напишите нам, пообщаемся :).


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Date Processing Attracts Bugs or 77 Defects in Qt 6.

Подробнее..

PVS-Studio, Blender цикл заметок о пользе регулярного использования статического анализа

03.03.2021 20:06:54 | Автор: admin

PVS-Studio мониторит код Blender
В статьях мы регулярно повторяем важную мысль: статический анализатор должен использоваться регулярно. В этом случае многие ошибки выявляются на самом раннем этапе, а их исправление максимально дёшево. Однако теория это одно, но намного лучше подкреплять слова практическими примерами. Рассмотрим несколько свежих ошибок, появившихся в новом коде проекта Blender.


Недавно мы настроили регулярную проверку проекта Blender, о чём мой коллега рассказал в статье "Just for fun: команда PVS-Studio придумала мониторить качество некоторых открытых проектов". В дальнейшем планируем начать мониторить ещё некоторые интересные проекты.


Сразу скажу, что мы не ставим перед собой задачу найти как можно больше ошибок. Целью является периодическое написание небольших заметок (таких как эта), в которых мы будем на практике показывать достоинства регулярного анализа кода. Другими словами, мы иногда будем описывать некоторые интересные ошибки в новом коде, найденные при очередном ночном запуске PVS-Studio, и тем самым популяризировать правильное использование методологии статического анализа кода.


Итак, давайте посмотрим, что найдено в свежем коде проекта Blender.


Фрагмент первый: double-checked locking


typedef struct bNodeTree {  ....  struct NodeTreeUIStorage *ui_storage;} bNodeTree;static void ui_storage_ensure(bNodeTree &ntree){  /* As an optimization, only acquire a lock if the UI storage doesn't exist,   * because it only needs to be allocated once for every node tree. */  if (ntree.ui_storage == nullptr) {    std::lock_guard<std::mutex> lock(global_ui_storage_mutex);    /* Check again-- another thread may have allocated the storage       while this one waited. */    if (ntree.ui_storage == nullptr) {      ntree.ui_storage = new NodeTreeUIStorage();    }  }}

Предупреждение PVS-Studio. V1036: Potentially unsafe double-checked locking. node_ui_storage.cc 46


Перед нами неправильная реализация блокировки с двойной проверкой. Для пояснения проблемы процитирую фрагмент статьи "C++ and the Perils of Double-Checked Locking", написанной Scott Meyers и Andrei Alexandrescu ещё в 2004 году. Как видите, проблема давно известна, но это не защищает разработчиков от того, чтобы наступать на одни и те же грабли. Хорошо, что анализатор PVS-Studio помогает выявлять подобные проблемы :). Итак, фрагмент из статьи:


Consider again the line that initializes pInstance: pInstance = newSingleton;

This statement causes three things to happen:

Step 1: Allocate memory to hold a Singleton object.

Step 2: Construct a Singleton object in the allocated memory.

Step 3: Make pInstance point to the allocated memory.

Of critical importance is the observation that compilers are not constrainedto perform these steps in this order! In particular, compilers are sometimes allowed to swap steps 2 and 3. Why they might want to do that is a question we'll address in a moment. For now, let's focus on what happens if they do.

Consider the following code, where we've expanded pInstance's initialization line into the three constituent tasks we mentioned above and where we've merged steps 1 (memory allocation) and 3 (pInstance assignment) into a single statement that precedes step 2 (Singleton construction). The idea is not that a human would write this code. Rather, it's that a compiler might generate code equivalent to this in response to the conventional DCLP source code (shown earlier) that a human would write.

Если вы хотите подробнее разобраться с написанием блокировки с двойной проверкой, то отсылаю вас к описанию диагностики и статьи, ссылки на которые были даны выше. Для нас важно другое.


Подобные ошибки очень коварны! Они могут очень редко проявлять себя. Программа вроде как работает, проходит все тесты и так далее. Но время от времени она неожиданно падает у пользователей, и понять причину может быть крайне проблематично. Воспроизвести такую ошибку может быть очень сложно. Другим словами, исправление этой ошибки по жалобе от пользователей может обойтись в 1000 раз дороже, чем правка кода, сделанная по результату анализа кода с помощью PVS-Studio или аналогичного инструмента.


Примечание. Ошибка может и не существовать сейчас в двоичном коде, и всё зависит от компилятора, и ключей оптимизации. Однако, даже если сейчас всё работает хорошо, это не значит, что так будет всегда. Ошибка может проявить себя при смене компилятора/ключей оптимизации.


Фрагмент второй: realloc


static void icon_merge_context_register_icon(struct IconMergeContext *context,                                             const char *file_name,                                             struct IconHead *icon_head){  context->read_icons = realloc(context->read_icons,    sizeof(struct IconInfo) * (context->num_read_icons + 1));  struct IconInfo *icon_info = &context->read_icons[context->num_read_icons];  icon_info->head = *icon_head;  icon_info->file_name = strdup(path_basename(file_name));  context->num_read_icons++;}

Анализатор PVS-Studio выдаёт здесь два предупреждения, и это правильно. Здесь действительно допущено сразу две ошибки различного плана.


Первая: V701: realloc() possible leak: when realloc() fails in allocating memory, original pointer 'context->read_icons' is lost. Consider assigning realloc() to a temporary pointer. datatoc_icon.c 252


Если память не удастся выделить, функция realloc вернёт значение NULL. Нулевой указатель будет записан в переменную context->read_icons, а её предыдущее значение будет потеряно. Раз предыдущее значение указателя потеряно, то и невозможно освободить ранее выделенный блок памяти, на который ссылался этот указатель. Произойдёт утечка памяти.


Вторая: V522: There might be dereferencing of a potential null pointer 'context->read_icons'. Check lines: 255, 252. datatoc_icon.c


Описанная выше ошибка в каком-то смысле и не ошибка с точки зрения автора кода. Он и не собирался писать код, который будет продолжать работу в случае невозможности увеличения блока выделенной памяти. Такой случай просто не рассматривается. Автор предполагает, что, если память выделить не удалось, программа просто аварийно завершится при разыменовании нулевого указателя. И поэтому смело работает с указателем, не выполняя его предварительную проверку. Оставим в стороне вопрос, насколько красивым является такое поведение программы. На мой взгляд, такое поведение библиотек недопустимо.


Интереснее другое. На самом-то деле падение может и не произойти. Запись производится вовсе не по нулевому указателю, а куда-то дальше. Теоретически возможна ситуация, когда этот адрес уже не находится в странице памяти, защищенной от записи, и никакого падения не будет. Будут испорчены какие-то случайные данные в памяти, и программа продолжит своё выполнение. Последствия работы с испорченными данными непредсказуемы. Подробнее всё это разобрано в статье "Почему важно проверять, что вернула функция malloc".


Фрагмент третий: разыменование указателя для проверки


static int node_link_invoke(bContext *C, wmOperator *op, const wmEvent *event){  ....  bNodeLinkDrag *nldrag = node_link_init(bmain, snode, cursor, detach);  nldrag->last_picked_multi_input_socket_link = NULL;  if (nldrag) {    op->customdata = nldrag;  ....}

Предупреждение PVS-Studio: V595: The 'nldrag' pointer was utilized before it was verified against nullptr. Check lines: 1037, 1039. node_relationships.c


Один из самых частых паттернов ошибок (proof). В начале указатель nldrag разыменовывается. Но из следующего условного оператора становится видно, что на самом деле этот указатель может быть нулевым.


Всё просто и понятно. Но, согласитесь, намного лучше поправить такую ошибку сразу, ещё на этапе написания кода, а не после того, когда на неё наткнётся тестировщик или пользователь.


Кстати, нашлась ещё одна такая-же ошибка, но описывать её неинтересно. Приведу только сообщение: V595: The 'seq' pointer was utilized before it was verified against nullptr. Check lines: 373, 385. strip_add.c


Заключение


Используйте статические анализаторы кода регулярно. От этого выиграют как разработчики, так и пользователи. Вы можете скачать и попробовать PVS-Studio здесь. Спасибо за внимание.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio, Blender: Series of Notes on Advantages of Regular Static Analysis of Code.

Подробнее..

Новые возможности по поиску угроз безопасности и защищённости в PVS-Studio 7.12

12.03.2021 12:15:29 | Автор: admin

Безопасность. Что для вас значит это слово? В наше время компании тратят много усилий для того, чтобы их продукт был защищён от взлома и разного рода утечек информации. PVS-Studio решил помогать своим пользователям в данном направлении и расширять функционал в этой области. Поэтому одним из главных новшеств предстоящего релиза станет появление в анализаторе новых возможностей обеспечения безопасности и защищённости кода. Об этом и будет рассказано в данной статье.

Пару слов о нас в плане безопасности и защищённости

На данный момент PVS-Studio развивается не только как статический анализатор для поиска дефектов качества кода (quality control решение), но и как решение для поиска дефектов защищённости (security) и безопасности (safety). В контексте защищённости мы являемся SAST-решением. SAST (Static Application Security Testing) это разновидность статического анализа кода, ориентированная на поиск потенциальных уязвимостей защищённости. Такой анализ может выявить большое количество дефектов, в том числе даже тех, которые не успели проявить себя. Что касается безопасности, то это уже другое направление, которое ориентировано на обеспечение надёжности и отказоустойчивости программ.

Как вы понимаете из названия данной статьи, мы расширяем свой функционал в данных областях. Ранее у нас уже были различные таблицы соответствия стандартам безопасности и защищённости на нашем сайте. Но пользоваться этим было не очень удобно, ведь в непосредственно результаты работы анализатора эта информация не попадала. Теперь же мы не только делаем использование данных возможностей анализатора более дружелюбным для пользователей (например, путём интеграции в интерфейсы наших IDE плагинов), но и расширяем уже имеющуюся базу, добавляя поддержку новых стандартов. Дополнительным толчком к этому для нас стало упоминание PVS-Studio в отчете Static Application Security Testing, Q3 2020 от компании Forrester Research одного из ведущих исследователей влияния новых и инновационных технологий на бизнес-процессы и рынок. Подробнее об этом и том, как мы развились в SAST и safety решение, вы можете почитать тут.

Новые возможности

Ну и чтобы не тянуть время, давайте сразу обозначим, что конкретно было добавлено. Итак, новое, безопасное и классное в PVS-Studio:

  • В анализатор добавлены новые группы диагностик OWASP ASVS и The AUTOSAR C++14 Coding Guidelines. Раньше соответствие диагностических правил PVS-Studio данным стандартам было доступно только на нашем сайте. Новых диагностических правил получилось более 50 штук!

  • В результатах работы анализатора теперь выдаётся информация о соответствии срабатываний стандарту безопасного написания кода SEI CERT. Раньше эта информация также была доступна только на сайте PVS-Studio.

  • Доработан интерфейс наших плагинов для Visual Studio, JetBrains Rider, IntelliJ IDEA для удобной работы с сообщениями анализатора, имеющими идентификаторы стандартов безопасности и защищённости.

  • Поддержаны новые группы диагностик (OWASP, AUTOSAR) в PlogConverter.

  • Поддержаны новые диагностики (OWASP, AUTOSAR) в SonarQube на уровне тегов. Провели работу по классификации наших диагностических правил по OWASP Top 10.

Примечание. В предыдущих версиях уже были поддержаны такие стандарты безопасности, как MISRA C:2012 и MISRA C++:2008. Для них на момент написания статьи реализовано 74 диагностических правила.

Ещё у нас поддержано соответствие наших диагностик наиболее распространённой классификации потенциальных уязвимостей CWE (Common Weakness Enumeration). Количество диагностик, которые подходят под данную классификацию у нас уже 514.

Новые группы диагностик

Поговорим немного о новых группах диагностик (OWASP и AUTOSAR), которые ранее присутствовали только на нашем сайте в виде сопоставлений. В новом релизе PVS-Studio 7.12 мы добавляем диагностики из этих стандартов в виде отдельных групп правил со своими номерами, документацией и всеми остальными присущими нашим диагностическим правилам вещами. То есть, проверяя проект, анализатор вам будет выдавать предупреждения для новых групп, по аналогии с остальными предупреждениями. Раньше из всех security и safety правил отдельные группы были только у диагностик PVS-Studio, соответствующих стандартам MISRA C и C++.

А вообще, что это за слова такие странные: OWASP, AUTOSAR? Давайте немного разъясним ситуацию.

The AUTOSAR C++14 Coding Guidelines это набор руководств для написания кода на языке C++14, который предназначен для работы в системах, где важны безопасность и отказоустойчивость. Основной сферой применения данного документа является автомобильная промышленность. Но он также может быть использован и в других отраслях, занимающихся разработкой встраиваемых систем.

Для данного стандарта мы создали отдельную группу и выделили ей номера с 3500 по 3999. Сопоставление этих диагностик со стандартом AUTOSAR вы можете посмотреть здесь.

OWASP Application Security Verification Standard это список требований к безопасности приложений и тестов, которые могут использоваться архитекторами программного обеспечения (ПО), разработчиками, тестировщиками, специалистами по защищённости приложений, продавцами и пользователями инструментов для разработки, сборки, тестирования и верификации защищённых приложений.

Как вы поняли, в отличие от стандарта организации AUTOSAR, OWASP ASVS не привязан к какому-то конкретному языку. Поэтому у нас реализованы диагностики этого типа на всех анализируемых нами языках (C, C++, C#, Java). Данные диагностические правила получили свою группу и номера с 5000 по 5999.

Теперь перейдём к CERT. SEI CERT Coding Standard это набор стандартов написания ПО для повышения надёжности и безопасности ПО на языках C, C++, Java и Perl. Эти стандарты разрабатываются координационным центром CERT (CERT Coordination Center, CERT/CC). Их сопоставление с правилами PVS-Studio представлено здесь.

Однако, в случае с CERT, мы не стали создавать новую группу диагностик. Причиной этому послужило то, что под этот стандарт попадает значительная часть наших диагностик общего назначения (General Analysis). Но не переживайте, информацию о том, что диагностика это конкретное CERT правило, вы всё равно узнаете. Она точно так же добавляется в результат работы анализатора, как OWASP ASVS или AUTOSAR C++14 Coding Guidelines.

При этом у нас продолжается поддержка таких стандартов, как MISRA C:2012 и MISRA C++:2008. Это стандарты разработки программного обеспечения, основная цель которых улучшить безопасность, переносимость и надёжность программ для встраиваемых систем (сопоставление).

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

В плагинах

Ну вот мы и добавили новые диагностики. А где же посмотреть результат их работы? Ну конечно же, в наших плагинах! На сегодняшний день мы поддерживаем отображение информации о стандартах безопасности в плагинах для трех IDE. Это Visual Studio (для версий с 2010 по 2019), JetBrains Rider и IntelliJ IDEA. Чтобы плагины могли отображать эти новые срабатывания, были сделаны следующие доработки:

  • Добавлен новый столбец SAST, в который выводится вся информация о MISRA C:2012, MISRA C++:2008, The AUTOSAR C++14 Coding Guidelines, OWASP ASVS, SEI CERT Coding Standard из предупреждений.

  • Убран столбец MISRA. Теперь вся информация заносится в столбец SAST. Этот же столбец будет использоваться в будущем и при поддержке нами новых стандартов.

  • Добавлены кнопки для новых стандартов, которые позволяют вам убирать соответствующие предупреждения из отображения. В настройках добавлена возможность полного или частичного выключения\включения диагностических правил для этих категорий.

Приведу пару картинок, чтобы вы имели представление, как это выглядит. В плагине для Visual Studio 2019 это выглядит вот так:

Точно такой же функционал мы добавили в Rider и в IntelliJ IDEA. Вот так это выглядит в Rider:

PlogConverter

Мы не могли забыть про нашу утилиту, которая позволяет конвертировать отчеты в различные форматы. Теперь все наши типы отчётов, в которые можно конвертировать результаты работы анализатора, поддерживают OWASP и AUTOSAR. Для примера посмотрим на, возможно, самый часто используемый тип для конвертации FullHtml. Этот тип позволяет вам изучать отчет в браузере: красиво и удобно, если нет возможности напрямую работать с плагином в вашей среде разработки. Плюс такой отчёт или ссылку на него удобно рассылать по почте.

Собственно, быстренько получили нужный файл и давайте теперь его посмотрим. Как вы видите, в заголовке можно увидеть новое поле Total Warnings (OWASP), которое говорит, сколько у вас потенциальных ошибок из этой категории:

Вот так отображается сам SAST столбец:

SonarQube

Хочется немного сказать и о нашей интеграции с SonarQube. Мы предоставляем плагин, который позволяет добавлять сообщения, найденные анализатором PVS-Studio, в базу сообщений сервера SonarQube. Далее вы можете фильтровать сообщения, осуществлять навигацию по коду для анализа ошибок, анализировать динамику количества ошибок, оценивать уровень качества кода проекта и так далее.

На данный момент мы расширяем возможности нашего плагина, добавляя теги для диагностик, относящихся к OWASP, AUTOSAR группам. Сейчас сообщения группы OWASP выглядят вот так:

Также мы провели работу по классификации наших диагностических правил по OWASP Top 10. OWASP Top 10 это рейтинг самых опасных векторов атак на веб-приложения. Каждое место в этом рейтинге имеет описание и примеры сценариев атаки, а также ссылки на правила из стандарта OWASP ASVS и классификацию CWE, которые к нему относятся. Для примера вы можете посмотреть, как выглядит одно из мест рейтинга.

В OWASP Top 10 входят такие уязвимости, как:

  1. инъекции;

  2. сломанная аутентификация;

  3. раскрытие конфиденциальных данных;

  4. внешние объекты XML;

  5. нарушенный контроль доступа;

  6. неверная конфигурация безопасности;

  7. межсайтовый скриптинг;

  8. небезопасная десериализация;

  9. использование компонентов с известными уязвимостями;

  10. недостаточное ведение журнала и мониторинг.

В SonarQube же они отображаются вот здесь:

Сделано это по аналогии с тем, как у нас уже отображается CWE, который вы тоже можете видеть на скриншоте. Для этого мы используем специальную вкладку Security Category. Приведу пример, как выглядит заполненный CWE:

Заключение

Как вы видите, этот релиз у нас вышел достаточно насыщенным. Анализатор получил новые группы диагностик для стандартов OWASP ASVS и AUTOSAR C++14 Coding Guidelines. В результаты работы анализатора дополнительно стала выводиться информация о соответствии срабатываний стандарту SEI CERT. Интерфейс наших плагинов (Visual Studio, JetBrains Rider, IntelliJ IDEA) обновился для удобной работы с сообщениями анализатора, имеющими идентификаторы стандартов безопасности и защищённости. Ещё и PlogConverter с SonarQube научились работать с новыми группами диагностик (OWASP, AUTOSAR). И все это только то, что касается направления безопасности и защищённости!

Данное направление очень важно. Ведь устранение всякого рода уязвимостей на этапе разработки программного обеспечения снижает шанс возникновения угроз безопасности в будущем. Благодаря этому компании не придётся нести финансовые потери или терять свой рейтинг. Поэтому мы стараемся помогать своим пользователям избежать проблем, связанных с безопасностью и защищённостью.

Будьте счастливы и следите за состоянием своего кода. Спасибо за внимание!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikolay Mironov, Paul Eremeev. PVS-Studio 7.12 New Features for Finding Safety and Security Threats.

Подробнее..

Пример, как в PVS-Studio появляются новые диагностики

20.03.2021 18:12:42 | Автор: admin

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


Всё началось с проверки проекта COVID-19 CovidSim Model и статьи про неинициализированную переменную. Проект оказался маленьким и написанным с использованием современного стандарта языка C++. Это значит, что он отлично может пополнить базу тестовых проектов для регрессионного тестирования ядра анализатора PVS-Studio.


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


Программист, которому поручили добавить проект в тестовую базу, подошёл к задаче основательно и решил заглянуть в раздел MISRA-диагностик. Вообще, это делать было необязательно, так как это весьма специфичная группа диагностик, которую нет смысла включать для таких проектов, как CovidSim.


MISRA С и MISRA C++ диагностики предназначены для разработчиков встраиваемых систем, и их суть сводится к ограничению использования небезопасных конструкций программирования. Например, не рекомендуется применять оператор goto (V2502), так как он провоцирует создание сложного кода, в котором легко допустить логическую ошибку. Подробнее с философией стандарта кодирования MISRA можно познакомиться в статье "Что такое MISRA и как её готовить".


Для прикладного программного обеспечения, которым как раз и является проект CovidSim, включать набор MISRA диагностик не имеет смысла. Пользователь просто утонет в огромном количестве малополезных для него сообщений. Например, экспериментируя с этим набором диагностик, мы получали более миллиона предупреждений для некоторых открытых проектов среднего размера. Грубо говоря, с точки зрения MISRA, в каждой третьей строчке кода может быть что-то не так :). Естественно, никто всё это смотреть и тем более править не будет. Проект или сразу разрабатывается с учётом MISRA рекомендаций, или для него этот стандарт кодирования неактуален.


Но мы отвлеклись от темы. Так вот, бегло просматривая MISRA предупреждения, коллега зацепился взглядом за предупреждение V2507, выданное для этого фрагмента кода.


if (radiusSquared > StateT[tn].maxRad2) StateT[tn].maxRad2 = radiusSquared;{  SusceptibleToLatent(a->pcell);  if (a->listpos < Cells[a->pcell].S)  {    UpdateCell(Cells[a->pcell].susceptible, a->listpos, Cells[a->pcell].S);    a->listpos = Cells[a->pcell].S;    Cells[a->pcell].latent[0] = ai;  }}StateT[tn].cumI_keyworker[a->keyworker]++;

Правило V2507 заставляет оборачивать тела условных операторов в фигурные скобки.


В первый момент наш дотошный коллега подумал, что анализатор дал сбой. Ведь здесь есть блок текста в фигурных скобках! Перед нами ложное срабатывание?


Давайте присмотримся. Код только кажется корректным, но таковым не является! Фигурные скобки не относятся к оператору if.


Отформатируем код для наглядности:


if (radiusSquared > StateT[tn].maxRad2)  StateT[tn].maxRad2 = radiusSquared;{  SusceptibleToLatent(a->pcell);  ....}

Согласитесь, это красивый баг. Он наверняка войдёт в Top10 C++ ошибок, найденных нами в 2021 году.


И что из этого следует? Подход, диктуемый стандартом MISRA, работает! Да, он заставляет писать везде фигурные скобки. Да, это утомительно. Но это оправданная плата за повышение надёжности встроенных приложений, используемых в медицинской технике, автомобилях и других системах высокой ответственности.


Хорошо, у разработчиков, использующих стандарт MISRA, всё нормально. Однако рекомендовать всем подряд использовать фигурные скобки это плохая идея. Используя такой подход, очень легко довести анализатор до состояния, когда им станет невозможно пользоваться. Будет так много сообщений, что их просто никто не будет смотреть.


Вот мы и добрались для появления идеи для новой диагностики общего назначения. Можно сформулировать следующее правило.


Выдать предупреждение, если для оператора if выполняются следующие условия:


  • весь условный оператор if записан в одну строчку и имеет только then-ветку;
  • следующий statement после if это compound statement, и он находится не на той же строке, что и if.

Заранее можно прогнозировать, что получится хорошее правило с низким количеством ложных срабатываний.


Именно так эта идея сейчас оформлена в нашей системе учёта задач. Возможно, в процессе реализации что-то будет сделано по-другому, но это уже неважно. Главное, появится хорошее диагностическое правило, которое начнёт выявлять новый паттерн ошибки. Далее мы распространим его на C# и Java ядра анализатора PVS-Studio.


Только что мы рассмотрели интересный пример, как было сформулировано новое диагностическое правило, которое затем будет реализовано в PVS-Studio. Скажем спасибо проекту CovidSim, стандарту кодирования MISRA и наблюдательности нашего коллеги.


Спасибо за внимание и следуйте за мной в мир С++ и багов :). Twitter. Facebook.


Дополнительный ссылки:


  1. Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей.
  2. Под капотом PVS-Studio для Java: разработка диагностик.
  3. Использование машинного обучения в статическом анализе исходного кода программ.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Example of How New Diagnostics Appear in PVS-Studio.

Подробнее..

Как PVS-Studio защищает от поспешных правок кода

24.03.2021 12:04:07 | Автор: admin

Недостижимый код
Хотя только недавно была заметка про проект CovidSim, есть хороший повод вновь про него вспомнить и продемонстрировать пользу регулярного использования PVS-Studio. Бывает, что все мы спешим и вносим правки в код, потеряв сосредоточенность. Статический анализатор может оказаться здесь хорошим помощником.


Всё началось с написания вот этих двух небольших заметок про открытый проект COVID-19 CovidSim Model:



После этого мы подумали и решили мониторить и этот проект с целью популяризации регулярного использования статических анализаторов кода. Подробнее про эту затею мой коллега писал в этой статье.


Только мы начали проверять этот проект, как результаты не заставили себя ждать :). Сейчас я покажу свежую ошибку, вызванную недавними неаккуратными правками кода. Конечно, мы не станем описывать в дальнейшем каждый баг и недочёт, обнаруженный анализатором. Будем писать только про что-то интересное.


Вот что получилось, после недавних модификаций файла CovidSim.cpp:


Правки


Кто-то решил создавать массивы не на стеке, а в куче. Но был невнимателен, внося изменения. Обратите внимание, что освобождение памяти находится после оператора return:


int GetXMLNode(....){  char* buf = new char[65536];  char* CloseNode = new char[2048];  char* CloseParent = new char[2048];  ....  if (ResetFilePos) fseek(dat, CurPos, 0);  return ret;  delete[] buf;  delete[] CloseNode;  delete[] CloseParent;}

В результате перед нами фрагмент недостижимого кода (unreachable code). И заодно утечка памяти.


Хорошо, что PVS-Studio тут же сообщает про эту ошибку: V779 Unreachable code detected. It is possible that an error is present. CovidSim.cpp 675


Используйте статический анализ регулярно и сможете устранить множество ошибок на самом раннем этапе! Согласитесь, лучше сразу исправить этот мелкий баг, чем гадать, почему вдруг программа начала потреблять слишком много оперативной памяти.


И последнее, что хочется отметить. Эта ошибка была бы невозможна, если не вручную управлять выделением и освобождением памяти, а применить RAII подход и использовать умные указатели.


Правильный и надёжный вариант кода:


std::unique_ptr<char[]> buf(new char[65536]);std::unique_ptr<char[]> CloseNode(new char[2048]);std::unique_ptr<char[]> CloseParent(new char[2048]);

Спасибо за внимание. Следуйте за мной в мир С++ и багов :). Twitter. Facebook.

Подробнее..

Теперь PVS-Studio ещё лучше знает, что за зверь такой strlen

27.04.2021 16:10:26 | Автор: admin

0824_DataFlow_And_Strlen_ru/image1.png
Как-то так несправедливо сложилось, что мы почти не уделяем в наших заметках внимание усовершенствованию внутренних механизмов анализатора, в отличие от новых диагностик. Поэтому давайте для разнообразия познакомимся с новым полезным усовершенствованием, коснувшимся анализа потока данных.


Всё началось с твита от JetBrains CLion IDE


На днях я увидел в Twitter пост от JetBrains про новые возможности статического анализатора, встроенного в CLion.


0824_DataFlow_And_Strlen_ru/image2.png


Поскольку мы скоро планируем выпустить плагин PVS-Studio для CLion, то я не мог пройти мимо и не написать, что мы тоже не лыком шиты. И что есть смысл попробовать PVS-Studio как плагин для CLion, чтобы находить ещё больше ошибок.


0824_DataFlow_And_Strlen_ru/image3.png


Ну и ещё немного с ними мило попереписывался:



После всего этого я подумал. А ведь они молодцы! Улучшили анализ потока данных и рассказывают миру. А мы чем хуже? Мы ведь тоже постоянно что-то улучшаем внутри анализатора, в том числе и тот же механизм анализа потока данных. И вот я уже пишу эту заметку.


А что у нас интересного с Data Flow


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


bool foo(){  unsigned N = 2;  for (unsigned i = 0; i < N; ++i)  {    bool stop = (i - 1 == N);    if (stop)      return true;  }  return false;}

Анализатор не мог понять, что переменной stop всегда присваивается значение false.


Почему false? Давайте быстро посчитаем:


  • диапазон значения переменной i = [0; 1];
  • возможные значения выражения i-1 = [0; 0] U [UINT_MAX; UINT_MAX];
  • значение переменной N, равное двойке, не входит в множество { 0, UINT_MAX };
  • условие всегда ложно.

Примечание. Неопределённого поведения здесь нет, так как происходит переполнение (wrap) при работе с беззнаковым типом.


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


Например, возникли ложные срабатывания, связанные с обработкой длины строк. Борьба с ними привела к новым улучшениям и обучению анализатора лучше понимать, как и зачем используют такие функции, как strlen. Сейчас мы на практике покажем, о каких улучшениях идёт речь.


В тестовой базе открытых проектов, на которой мы регулярно проводим регрессионное тестирование ядро анализатора, есть эмулятор FCEUX. После проделанных улучшений мы смогли найти в коде интересную ошибку в функции Assemble.


int Assemble(unsigned char *output, int addr, char *str) {  output[0] = output[1] = output[2] = 0;  char astr[128],ins[4];  if ((!strlen(str)) || (strlen(str) > 0x127)) return 1;  strcpy(astr,str);  ....}

Видите ошибку? Если честно, мы не сразу её заметили и думали, что что-то поломали. А когда поняли, в чём суть, то в очередной раз восхитились тем, как статический анализ может быть полезен.


Предупреждение PVS-Studio: V512 A call of the 'strcpy' function will lead to overflow of the buffer 'astr'. asm.cpp 21


Всё равно не видите ошибку? Давайте внимательно разберём код. Для начала уберём всё не относящееся к делу:


int Assemble(char *str) {  char astr[128];  if ((!strlen(str)) || (strlen(str) > 0x127)) return 1;  strcpy(astr,str);  ....}

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


Пока всё логично и правильно? На первый взгляд, да. Но что это?! Что это за константа 0x127?!


Это вовсе не 127. Совсем не 127 :)


Константа задана в шестнадцатеричной системе. Если перевести в десятичную, то получается 295.


Итак, написанный код эквивалентен следующему:


int Assemble(char *str) {  char astr[128];  if ((!strlen(str)) || (strlen(str) > 295)) return 1;  strcpy(astr,str);  ....}

Как видите, проверка никак не защищает от переполнения буфера, и анализатор совершенно правильно предупреждает о проблеме.


Раньше анализатор не мог найти ошибку, будучи не в состоянии понять, что две функции strlen работают с одной строкой. И эта строка не меняется между двумя вызовами strlen. С точки зрения программиста, всё это очевидно, а вот анализатор нужно учить всё это понимать :).


Теперь PVS-Studio выводит из выражения, что длина строки str лежит в диапазоне [1..295], а значит, может возникнуть выход за границу массива, если попытаться его скопировать в буфер astr.


0824_DataFlow_And_Strlen_ru/image4.png


Новые вызовы


Описанная ошибка присутствует и в текущей версии кодовой базы проекта FCEUX. Но мы её не найдём, так как код изменился и теперь длина строки сохраняется в переменной. Это разрывает взаимосвязь между строкой и её длиной. Анализатор пока, к сожалению, молчит на новый вариант кода:


int Assemble(unsigned char *output, int addr, char *str) {  output[0] = output[1] = output[2] = 0;  char astr[128],ins[4];  int len = strlen(str);  if ((!len) || (len > 0x127)) return 1;  strcpy(astr,str);  ....}

Человеку такой код может показаться даже проще, но, с точки зрения статического анализа, он труден для отслеживания значений. Нужно учитывать, что значение переменной len является длиной строки str. Дополнительно требуется аккуратно отслеживать, когда разоврётся эта взаимосвязь при модификации содержимого строки или переменной len.


Пока это анализатор PVS-Studio делать не умеет. Зато видно, куда можно и нужно развиваться! Со временем научимся находить ошибку и в этом новом коде.


Кстати, читатель может задаться вопросом, а почему мы анализируем старый код проектов и не обновляем их регулярно? Всё просто. Если обновлять проекты, мы не сможем проводить регрессионное тестирование. Будет непонятно, появились изменения в работе анализатора из-за правки кода самого анализатора или из-за правки в коде проверяемого проекта. Поэтому мы не обновляем открытые проекты, используемые в качестве базы для тестирования.


А чтобы тестировать анализатор на современном коде, написанном на C++14, C++17 и т.д., мы постепенно пополняем базу новыми проектами. Например, относительно недавно мы добавили коллекцию header-only C++ библиотек (awesome-hpp).


Заключение


Развивать механизмы анализа потока данных интересно и полезно. А если вам интересней в целом больше узнать о работе статических анализаторов кода, то предлагаем вашему вниманию следующие наши публикации:


  1. Анализатор кода не прав, да здравствует анализатор
  2. Ложные срабатывания в PVS-Studio: как глубока кроличья нора
  3. Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей
  4. Использование машинного обучения в статическом анализе исходного кода программ

Приглашаю скачать анализатор PVS-Studio и проверить свои проекты.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio Learns What strlen is All About.

Подробнее..

Зачем PVS-Studio использует анализ потока данных по мотивам интересной ошибки в Open Asset Import Library

18.02.2021 18:15:13 | Автор: admin

PVS-Studio - Анализ потока данных
Анализ потока данных является неотъемлемой частью любого современного статического анализатора кода. Однако, со стороны, не очень понятно, что это и главное зачем нужно. До сих пор некоторые ассоциируют статический анализ с поиском чего-то в коде по определённому шаблону. Поэтому время от времени мы пишем заметки, в которых демонстрируем, как та или иная технология, используемая в анализаторе PVS-Studio, помогает выявить очередную интересную ошибку. Сегодня как раз такая статья, в которой мы рассмотрим баг в одной из реализаций стандарта кодирования двоичных данных Base64.


Всё началось с проверки свежей версии библиотеки Qt 6. Про это была отдельная классическая статья, где я описал 77 найденных ошибок. Так получилось, что вначале я решил бегло полистать отчёт, ещё не пряча предупреждения, относящиеся к сторонним библиотекам. Другими словами, я не отключил в настройках предупреждения, относящиеся к \src\3rdparty. И так вышло, что я сразу наткнулся на интересный пример ошибки в библиотеки Open Asset Import Library, про которую я решил сделать эту отдельную маленькую заметку.


Найденный дефект демонстрирует, зачем в инструментах, таких как PVS-Studio, полезно анализировать поток данных. Без этого поиск многих ошибок просто невозможен. Кстати, если вам интересно подробнее узнать про анализ потока данных и про другие аспекты устройства инструмента, предлагаю вашему вниманию статью "Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей".


Теперь перейдём, собственно, к ошибке, обнаруженной в Open Asset Import Library (assimp). Файл: \src\3rdparty\assimp\src\code\FBX\FBXUtil.cpp.


std::string EncodeBase64(const char* data, size_t length){    // calculate extra bytes needed to get a multiple of 3    size_t extraBytes = 3 - length % 3;    // number of base64 bytes    size_t encodedBytes = 4 * (length + extraBytes) / 3;    std::string encoded_string(encodedBytes, '=');    // read blocks of 3 bytes    for (size_t ib3 = 0; ib3 < length / 3; ib3++)    {        const size_t iByte = ib3 * 3;        const size_t iEncodedByte = ib3 * 4;        const char* currData = &data[iByte];        EncodeByteBlock(currData, encoded_string, iEncodedByte);    }    // if size of data is not a multiple of 3,    // also encode the final bytes (and add zeros where needed)    if (extraBytes > 0)    {        char finalBytes[4] = { 0,0,0,0 };        memcpy(&finalBytes[0], &data[length - length % 3], length % 3);        const size_t iEncodedByte = encodedBytes - 4;        EncodeByteBlock(&finalBytes[0], encoded_string, iEncodedByte);        // add '=' at the end        for (size_t i = 0; i < 4 * extraBytes / 3; i++)            encoded_string[encodedBytes - i - 1] = '=';    }    return encoded_string;}

Если хотите, то для начала можете попробовать обнаружить ошибку самостоятельно. А чтобы вы случайно сразу не прочитали ответ, приведу пока список некоторых других интересных статей и кратко расскажу, что такое Base64 :). Список дополнительных статей на близкую тематику:


  1. 31 февраля;
  2. Использование машинного обучения в статическом анализе исходного кода программ;
  3. Как внедрить статический анализатор кода в legacy проект и не демотивировать команду.

Ok, продолжим. Перед нами реализация алгоритма кодирования строки байт в кодировку Base64. Это стандарт кодирования двоичных данных при помощи только 64 символов. Алфавит кодирования содержит текстово-цифровые латинские символы A-Z, a-z и 0-9 (62 знака) и 2 дополнительных символа, зависящих от системы реализации. Каждые 3 исходных байта кодируются 4 символами.


Если осталось закодировать только один или два байта, то в результате получаются только первые два или три символа строки, а выходная строка дополняется двумя или одним символами "=". Это предотвращает добавление дополнительных битов к восстановленным данным. Вот этот момент как раз реализован в рассматриваемой функции неправильно.


Если вы нашли ошибку, вы молодец. Если нет, то это тоже нормально. Нужно вникать в код, чтобы заметить, что что-то идёт не так. Анализатор про это "что-то не то" сообщает предупреждением: V547 [CWE-571] Expression 'extraBytes > 0' is always true. FBXUtil.cpp 224


Чтобы понять причину беспокойства анализатора, давайте посмотрим, как инициализируется переменная extraBytes:


// calculate extra bytes needed to get a multiple of 3size_t extraBytes = 3 - length % 3;

Программист планировал вычислить, сколько дополнительных байт входных данных нужно обработать, если их общее количество не равно 3. Для этого нужно просто поделить количество обрабатываемых байт по модулю 3. Правильный вариант инициализации переменной:


size_t extraBytes = length % 3;

Тогда, если обрабатывается, например, 5 байт, то получаем 5 % 3 = 2, и нужно дополнительно обработать 2 байта. Если на вход поступило 6 байт, то ничего отдельно обрабатывать не нужно, так как 6 % 3 = 0.


Но программист перемудрил и написал бессмысленный код:


size_t extraBytes = 3 - length % 3;

И как раз при анализе этого кода анализатору и понадобился механизм анализа потока данных. Какое бы значение не находилось в переменной length, после деления по модулю будет получено значение в диапазоне [0..2]. Анализатор PVS-Studio умеет работать с диапазонами, точными значениями и множествами. Т. е. речь идёт про Value Range Analysis. В данном случае будет использован именно диапазон значений.


Продолжим вычисления:


size_t extraBytes = 3 - [0..2];

Получается, что переменная extraBytes никогда не будет равна нулю. Анализатор вычислит следующий возможный диапазон её значений: [1..3].


До момента проверки, переменная нигде не изменяется. Следовательно, анализатор совершенно прав, предупреждая, что результатом проверки всегда будет истина:


if (extraBytes > 0)

Это простой, но красивый пример, когда анализ потока данных позволил вычислить диапазон значений переменной, проследить, что она не изменяется и, наконец, то, что условие всегда истинно.


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


// calculate extra bytes needed to get a multiple of 3size_t extraBytes = 3 - length % 3; // 3-6%3 = 3// number of base64 bytessize_t encodedBytes = 4 * (length + extraBytes) / 3; // 4*(6+3)/3 = 12std::string encoded_string(encodedBytes, '=');

Уже получилось, что выходная строка будет содержать 12 символов, а не 8. Дальше тоже всё будет работать неправильно даже нет смысла вдаваться в подробности.


Вот так легко и просто статический анализ нашёл ошибку в коде. А представьте, ведь кто-то будет мучиться и отлаживаться, чтобы понять, почему неправильно происходит кодирование символов в кодировку Base64. Здесь мы, кстати, подходим к вопросу качества используемых сторонних библиотек, который я рассматривал в публикации "Почему важно проводить статический анализ открытых библиотек, которые вы добавляете в свой проект".


Попробуйте внедрить регулярное использование PVS-Studio в ваш процесс разработки, чтобы находить многие ошибки на самом раннем этапе. Вам понравится :). Если вы разрабатываете открытый проект, то анализатор можно использовать бесплатно. Спасибо за внимание и безбажного вам кода.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Why PVS-Studio Uses Data Flow Analysis: Based on Gripping Error in Open Asset Import Library.

Подробнее..

Пример полезного комментария

22.03.2021 16:16:43 | Автор: admin

Пример полезного комментария


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


Код, который мы рассмотрим далее, был выписан в процессе работы над статьёй "Обработка дат притягивает ошибки или 77 дефектов в Qt 6".


Анализатор PVS-Studio обратил внимание на этот фрагмент кода, выдав предупреждение: V575 [CWE-628] The 'memcpy' function doesn't copy the whole string. Use 'strcpy / strcpy_s' function to preserve terminal null. qplaintestlogger.cpp 253. Собственно, вот он:


const char *msgFiller = msg[0] ? " " : "";QTestCharBuffer testIdentifier;QTestPrivate::generateTestIdentifier(&testIdentifier);QTest::qt_asprintf(&messagePrefix, "%s: %s%s%s%s\n",                   type, testIdentifier.data(), msgFiller, msg,                   failureLocation.data());// In colored mode, printf above stripped our nonprintable control characters.// Put them back.memcpy(messagePrefix.data(), type, strlen(type));outputMessage(messagePrefix.data());

Обратите внимание на вызов функции memcpy. Сам по себе этот код вызывает сразу два вопроса:


  1. Зачем что-то записывается в буфер, содержимое которого было только что сформировано с помощью printf-подобной функции?
  2. Точно не ошибка, что не копируется терминальный ноль? Это как раз и не нравится анализатору.

К счастью, комментарий сразу всё проясняет. Нужно восстановить некие непечатаемые символы.


Перед нами нужный и полезный текст. Это отличный образец комментария, поясняющего неочевидный момент в коде. Можно приводить его как пример в обучающих статьях.


Для сравнения рассмотрим другой фрагмент кода из этого же файла:


char buf[1024];if (result.setByMacro) {  qsnprintf(buf, sizeof(buf), "%s%s%s%s%s%s\n", buf1, bufTag, fill,            buf2, buf2_, buf3);} else {  qsnprintf(buf, sizeof(buf), "%s%s%s%s\n", buf1, bufTag, fill, buf2);}memcpy(buf, bmtag, strlen(bmtag));outputMessage(buf);

Здесь забыли сделать аналогичный комментарий. И картина радикально меняется. Этот код способен ввести в замешательство нового члена команды, который будет его сопровождать или модифицировать. Совершенно не понятно, зачем нужен этот memcpy. Более того, непонятно, почему в начало строки печаталось содержимое некоего буфера buf1, а затем в начало строки помещается содержимое буфера bmtag. Как много вопросов, как мало ответов. Не стоит писать такой код.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. One Useful Comment.

Подробнее..

Подводные камни в бассейне строк, или ещё один повод подумать перед интернированием экземпляров класса String в C

06.04.2021 16:12:32 | Автор: admin

Будучи разработчиками программного обеспечения, мы всегда хотим, чтобы написанное нами ПО работало быстро. Использование оптимального алгоритма, распараллеливание, применение различных техник оптимизации мы будем прибегать ко всем известным нам средствам, дабы улучшить производительность софта. К одной из таких техник оптимизации можно отнести и так называемое интернирование строк. Оно позволяет уменьшить объём потребляемой процессом памяти, а также значительно сокращает время, затрачиваемое на сравнение строк. Однако, как и везде в жизни, необходимо соблюдать меру не стоит использовать интернирование на каждом шагу. Далее в этой статье будет показано, как можно обжечься и создать своему приложению неочевидный bottleneck в виде метода String.Intern.

Для тех, кто мог забыть, напомню, что в C# строки являются ссылочным типом. Следовательно, сама строковая переменная это просто ссылка, которая лежит на стеке и хранит в себе адрес, указывающий на экземпляр класса String, расположенный на куче.

Есть несколько версий формулы для того, чтобы посчитать, сколько байт занимает строковый объект на куче: вариант от Джона Скита и вариант, показанный в статье Тимура Гуева. На картинке выше я использовал второй вариант. Даже если эта формула верна не на 100 %, мы всё равно можем приблизительно прикинуть размер строковых объектов. К примеру, для того чтобы занять 1 Гб оперативной памяти, достаточно будет, чтобы в памяти процесса было около 4,7 миллионов строк (каждая длиной в 100 символов). Если мы полагаем, что среди строк, с которыми работает наша программа, есть большое количество дубликатов, как раз и стоит воспользоваться встроенным во фреймворк функционалом интернирования. Давайте сейчас быстро вспомним, что вообще такое интернирование строк.

Интернирование строк

Идея интернирования строк состоит в том, чтобы хранить в памяти только один экземпляр типа String для идентичных строк. При старте нашего приложения виртуальная машина создаёт внутреннюю хэш-таблицу, которая называется таблицей интернирования (иногда можно встретить название String Pool). Эта таблица хранит ссылки на каждый уникальный строковый литерал, объявленный в программе. Кроме того, используя два метода, описанных ниже, мы сами можем получать и добавлять ссылки на строковые объекты в эту таблицу. Если наше приложение работает с большим количеством строк, среди которых часто встречаются одинаковые, то нет смысла каждый раз создавать новый экземпляр класса String. Вместо этого можно просто ссылаться на уже созданный на куче экземпляр типа String, получая ссылку на него путём обращения к таблице интернирования. Виртуальная машина сама интернирует все строковые литералы, встреченные в коде (более подробно про многие хитрости интернирования можно прочитать в этой статье). А нам для работы предоставляются два метода: String.Intern и String.IsInterned.

Первый на вход принимает строку и, если идентичная строка уже имеется в таблице интернирования, возвращает ссылку на уже имеющийся на куче объект типа String. Если же такой строки ещё нет в таблице, то ссылка на этот строковый объект добавляется в таблицу интернирования и затем возвращается из метода. Метод IsInterned также на вход принимает строку и возвращает ссылку из таблицы интернирования на уже имеющийся объект. Если такого объекта нет, то возвращается null (про неинтуитивно возвращаемое значение этого метода не упомянул только ленивый).

Используя интернирование, мы сокращаем количество новых строковых объектов путём работы с уже имеющимися через ссылки, полученные посредством метода Intern. Помимо того, что мы не создаём большого количества новых объектов и экономим память, мы ещё и увеличиваем производительность программы. Ведь если мы работаем с большим количеством строковых объектов, ссылки на которые быстро пропадают со стека, то это может провоцировать частую сборку мусора, что негативно скажется на общем быстродействии программы. Интернированные строки будут существовать до конца жизни процесса, даже если ссылок на эти объекты в программе уже нет. Это первый нюанс, к которому стоит отнестись с осторожностью, ведь изначальное стремление к использованию интернирования в целях уменьшения расхода памяти может привести к совершенно противоположному результату.

Интернирование строк может дать прирост производительности при сравнении этих самых строк. Взглянем на реализацию метода String.Equals:

public bool Equals(String value){  if (this == null)    throw new NullReferenceException();   if (value == null)    return false;   if (Object.ReferenceEquals(this, value))    return true;    if (this.Length != value.Length)    return false;   return EqualsHelper(this, value);}

До вызова метода EqualsHelper, где производится посимвольное сравнение строк, метод Object.ReferenceEquals выполняет проверку на равенство ссылок. Если строки интернированы, то уже на этапе проверки ссылок на равенство метод Object.ReferenceEquals вернёт значение true при условии равенства строк (без необходимости сравнения самих строк посимвольно). Конечно, если ссылки не равны, то в итоге произойдёт вызов метода EqualsHelper и последующее посимвольное сравнение. Ведь методу Equals неизвестно, что мы работаем с интернированными строками, и возвращаемое значение false методом ReferenceEquals уже свидетельствует о различии сравниваемых строк.

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

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

После того, как мы немного освежили память в вопросе интернирования строк, давайте же перейдём непосредственно к описанию той проблемы, с которой я столкнулся.

Краткая история того, как всё начиналось

В корпоративном bug tracker'е уже какое-то время значилась задача, в которой требовалось провести исследование: как распараллеливание процесса анализа С++ кода может сократить продолжительность анализа. Хотелось, чтобы анализатор PVS-Studio параллельно работал на нескольких машинах при анализе одного проекта. В качестве программного обеспечения, позволяющего проводить подобное распараллеливание, был выбран IncrediBuild. IncrediBuild позволяет параллельно запускать на машинах, находящихся в одной сети, различные процессы. Например, такой процесс, как компиляция исходных файлов, можно распараллелить на разные машины, доступные в компании (или в облаке), и таким образом добиться существенного сокращения времени сборки проекта. Данное программное обеспечение является довольно популярным решением среди разработчиков игр.

Моя задача заключалась в том, чтобы подобрать проект для анализа, проанализировать его с помощью PVS-Studio локально на своей машине, а затем запустить анализ уже с использованием IncrediBuild, который будет заниматься распараллеливанием процессов анализатора на машинах, доступных в расположении компании. По результатам подвести некий итог целесообразности такого распараллеливания. Если результат окажется положительным, предлагать нашим клиентам такое решения для ускорения анализа.

Был выбран open source проект Unreal Tournament. Удалось успешно уговорить программистов посодействовать в решении задачи и установить IncrediBuild на их машины. Полученный объединённый кластер включал около 145 ядер.

Так как я анализировал проект Unreal Tournament с помощью применения системы мониторинга компиляции в PVS-Studio, то мой сценарий работы был следующим. Я запускал программу CLMonitor.exe в режиме монитора, выполнял полную сборку проекта Unreal Tournament в Visual Studio. Затем, после прохождения сборки, опять запускал CLMonitor.exe, но уже в режиме запуска анализа. В зависимости от указанного в настройках PVS-Studio значения для параметра ThreadCount, CLMonitor.exe параллельно одновременно запускает соответствующее количество дочерних процессов PVS-Studio.exe, которые и занимаются анализом каждого отдельного исходного С++ файла. Один дочерний процесс PVS-Studio.exe анализирует один исходный файл, а после завершения анализа передаёт полученные результаты обратно в CLMonitor.exe.

Всё просто: выставил параметр ThreadCount в настройках PVS-Studio, равный количеству доступных ядер (145), запустил анализ и сел в ожидании того, что сейчас увижу, как 145 процессов PVS-Studio.exe будут исполняться параллельно на удалённых машинах. У приложения IncrediBuild есть удобная система мониторинга распараллеливания Build Monitor, через которую можно наблюдать, как процессы запускаются на удалённых машинах. Что-то подобное я и наблюдал в процессе анализа:

Казалось, что нет ничего проще: сиди и смотри, как проходит анализ, а после просто зафиксируй время его проведения с использованием IncrediBuild и без. Реальность оказалась несколько интересней...

Обнаружение проблемы, её локализация и устранение

Пока проходил анализ, можно было переключиться на выполнение других задач или просто медитативно поглядывать на бегущие полосы процессов PVS-Studio.exe в окне Build Monitor. После завершения анализа с использованием IncrediBuild я сравнил временные показатели продолжительности анализа с результатами замеров без применения IncrediBuild. Разница ощущалась, но общий результат, как мне показалось, мог бы быть и лучше: 182 минуты на одной машине с 8 потоками и 50 минут с использованием IncrediBuild и 145 потоками. Получалось, что количество потоков возросло в 18 раз, но при этом время анализа уменьшилось всего в 3,5 раза. Напоследок я опять решил взглянуть уже на итоговый результат окна Build Monitor. Прокручивая нижний ползунок, я заметил следующие аномалии на графике:

Было видно, что процессы PVS-Studio.exe запускались и успешно завершались, а затем по какой-то причине происходила некая пауза между запуском следующей партии процессов PVS-Studio.exe. И так повторялось несколько раз. Пауза за паузой. И эти простои между запусками складывались в ощутимую задержку и вносили свою лепту в общее время анализа. Первое, что пришло в голову, это вина IncrediBuild. Наверняка он выполняет какую-то свою внутреннюю синхронизацию между процессами и тормозит их запуск.

Я показал эти аномалии на графиках своему старшему коллеге. Он, в отличие от меня, не был так поспешен в выводах. Он предложил посмотреть на то, что происходит внутри нашего приложения CLMonitor.exe в момент того, как на графике начинают появляться видимые простои. Запустив анализ повторно и дождавшись первого очевидного "провала" на графике, я подключился к процессу CLMonitor.exe с помощью отладчика Visual Studio и поставил его на паузу. Открыв вкладку Threads, мы с коллегой увидели около 145 приостановленных потоков. Перемещаясь по местам в коде, где остановили своё исполнение данные потоки, мы увидели строки кода подобного содержания:

....return String.Intern(settings == null ? path                                 : settings                                 .TransformToRelative(path.Replace("/", "\\"),                                                      solutionDirectory));....analyzedSourceFiles.Add( String.Intern(settings                        .TransformPathToRelative(analyzedSourceFilePath,                                                  solutionDirectory))                       );....

Что общего мы видим в этих строках кода? В каждой из них используется метод String.Intern. Причём его применение кажется оправданным. Дело в том, что это те места, где CLMonitor.exe обрабатывает данные, приходящие от процессов PVS-Studio.exe. Обработка происходит путём записи данных в объекты типа ErrorInfo, инкапсулирующего в себе информацию о найденной анализатором потенциальной ошибке в проверяемом коде. Интернируем мы тоже вполне разумные вещи, а именно пути до исходных файлов. Один исходный файл может содержать множество ошибок, поэтому нет смысла, чтобы объекты типа ErrorInfo содержали в себе разные строковые объекты с одинаковым содержимым. Логично просто ссылаться на один объект из кучи.

После недолгих раздумий стало понятно, что интернирование здесь применено в очень неподходящий момент. Итак, вот какую ситуацию мы наблюдали в отладчике. Пока по какой-то причине 145 потоков висели на выполнении метода String.Intern, кастомный планировщик задач LimitedConcurrencyLevelTaskScheduler внутри CLMonitor.exe не мог запустить новый поток, который в дальнейшем бы породил новый процесс PVS-Studio.exe, а IncrediBuild уже запустил бы этот процесс на удалённой машине. Ведь, с точки зрения планировщика, поток ещё не завершил своё исполнение он выполняет преобразование полученных данных от PVS-Studio.exe в ErrorInfo с последующим интернированием. Ему всё равно, что сам процесс PVS-Studio.exe уже давно завершился и удалённые машины попросту простаивают без дела. Поток ещё активен, и установленный нами лимит в 145 потоков не даёт планировщику породить новый.

Выставление большего значения для параметра ThreadCount не решило бы проблему, так как это только увеличило бы очередь из потоков, висящих на исполнении метода String.Intern.

Убирать интернирование совсем нам не хотелось, так как это привело бы к увеличению объёма потребляемой оперативной памяти процессом CLMonitor.exe. В конечном счёте было найдено довольно простое и элегантное решение: перенести интернирование из потока, выполняющего запуск процесса PVS-Studio.exe в чуть более позднее место исполнения кода (в поток, который занимается непосредственно формированием отчёта об ошибках).

Как сказал мой коллега, обошлись хирургической правкой буквально двух строк и решили проблему с простаиванием удалённых машин. При повторных запусках анализа окно Build Monitor уже не показывало каких-либо значительных промежутков времени между запусками процессов PVS-Studio.exe. А время проведения анализа снизилось с 50 минут до 26, то есть почти в два раза. Теперь если смотреть на общий результат, который мы получили при использовании IncrediBuild и 145 доступных ядер, то общее время анализа уменьшилось в 7 раз. Это впечатляло несколько больше, нежели цифра в 3,5 раза.

String.Intern почему так медленно, или изучаем код CoreCLR

Стоит отметить, что как только мы увидели зависания потоков на местах вызова метода String.Intern, то почти сразу подумали, что под капотом у этого метода присутствует критическая секция с каким-нибудь lock'ом. Раз каждый поток может писать в таблицу интернирования, то внутри метода String.Intern должен быть какой-нибудь механизм синхронизации, чтобы сразу несколько потоков, выполняющих запись в таблицу, не перезаписали данные друг друга. Хотелось подтвердить свои предположения, и мы решили посмотреть имплементацию метода String.Intern на ресурсе reference source. Там мы увидели, что внутри нашего метода интернирования есть вызов другого метода *Thread.GetDomain().GetOrInternString(str)*. Что ж, давайте посмотрим его реализацию:

internal extern String GetOrInternString(String str);

Уже интересней. Этот метод импортируется из какой-то другой сборки. Из какой? Так как интернированием строк всё-таки занимается сама виртуальная машина CLR, то мой коллега направил меня прямиком в репозиторий среды исполнения .NET. Выкачав репозиторий, мы отправились к солюшену с названием CoreCLR. Открыв его и выполнив поиск по всему решению, мы нашли метод GetOrInternString с подходящей сигнатурой:

STRINGREF *BaseDomain::GetOrInternString(STRINGREF *pString)

В нём увидели вызов метода GetInternedString. Перейдя в тело этого метода, заметили код следующего вида:

....if (m_StringToEntryHashTable->GetValue(&StringData, &Data, dwHash)){  STRINGREF *pStrObj = NULL;  pStrObj = ((StringLiteralEntry*)Data)->GetStringObject();  _ASSERTE(!bAddIfNotFound || pStrObj);  return pStrObj;}else{  CrstHolder gch(&(SystemDomain::GetGlobalStringLiteralMap()                                   ->m_HashTableCrstGlobal));  ....  // Make sure some other thread has not already added it.  if (!m_StringToEntryHashTable->GetValue(&StringData, &Data))  {    // Insert the handle to the string into the hash table.    m_StringToEntryHashTable->InsertValue(&StringData, (LPVOID)pEntry, FALSE);  }  ....}....

Поток исполнения попадает в ветку else только в том случае, если метод, занимающийся поиском ссылки на объект типа *String *(метод GetValue) в таблице интернирования, вернёт false. Перейдём к рассмотрению кода, который представлен в ветке else. Интерес тут вызывает строка создания объекта типа CrstHolder с именем gch. Переходим в конструктор CrstHolder и видим код следующего вида:

inline CrstHolder(CrstBase * pCrst)    : m_pCrst(pCrst){    WRAPPER_NO_CONTRACT;    AcquireLock(pCrst);}

Видим вызов метода AcquireLock. Уже хорошо. Код метода AcquireLock:

DEBUG_NOINLINE static void AcquireLock(CrstBase *c){  WRAPPER_NO_CONTRACT;  ANNOTATION_SPECIAL_HOLDER_CALLER_NEEDS_DYNAMIC_CONTRACT;  c->Enter();}

Вот, собственно, мы и видим точку входа в критическую секцию вызов метода Enter. Оставшиеся сомнения в том, что это именно тот метод, который занимается блокировкой, у меня пропали после того, когда я прочитал оставленный к этому методу комментарий: "Acquire the lock". В дальнейшем погружении в код CoreCLR я не видел особого смысла. Получается, версия с тем, что при занесении новой записи в таблицу интернирования поток заходит в критическую секцию, вынуждая все другие потоки ожидать, когда блокировка спадёт, подтвердилась. Создание объекта типа CrstHolder, а следовательно, и заход в критическую секцию происходят сразу перед вызовом метода m_StringToEntryHashTable->InsertValue.

Блокировка пропадает сразу после того, как мы выходим из ветки else. Так как в этом случае для объекта gch вызывается его деструктор, который и вызывает метод ReleaseLock:

inline ~CrstHolder(){  WRAPPER_NO_CONTRACT;  ReleaseLock(m_pCrst);}

Когда потоков немного, то и простой в работе может быть небольшим. Но когда их количество возрастает, например до 145 (как в случае использования IncrediBuild), то каждый поток, пытающийся добавить новую запись в таблицу интернирования, временно блокирует остальные 144 потока, также пытающихся внести в неё новую запись. Результаты этих блокировок мы и наблюдали в окне Build Monitor.

Заключение

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

Благодарю за прочтение.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ilya Gainulin. Pitfalls in String Pool, or Another Reason to Think Twice Before Interning Instances of String Class in C#.

Подробнее..

Ядро macOS, есть ли червячки в этом яблоке?

29.03.2021 12:16:08 | Автор: admin

0818_XNU_MacOS_Kernel_ru/image1.png


В самом начале этого года Apple выложили в открытый доступ исходный код системных компонентов macOS 11.0 Big Sur, включая XNU ядро операционной системы macOS. Пару лет назад исходный код ядра уже проверялся PVS-Studio в связи с выходом анализатора для macOS. Прошло достаточно много времени, и вышел новый релиз исходного кода ядра. Почему бы и не провести повторную проверку.


Что это за проект, Apple и open-source?


XNU X is Not Unix используется и разрабатывается Apple в качестве ядра операционных систем OS X. Исходные коды этого ядра 20 лет назад были опубликованы под лицензией APSL (Apple Public Source License) вместе с OC Darwin. Раньше Darwin можно было даже установить в качестве полноценной операционной системы, однако теперь это стало невозможно. Причиной публикации исходного кода является тот факт, что он во многом основан на других open-source проектах.


Исходные коды компонентов можно найти тут. Для проверки я использовала зеркало проекта на GitHub.


Предыдущая проверка


Как я уже упомянула, этот проект ранее проверялся нами с помощью PVS-Studio. С предыдущими результатами можно познакомиться в статье: "Релиз PVS-Studio для macOS: 64 weaknesses в Apple XNU Kernel". После публикации мой коллега Святослав также отправил статью разработчикам на почту, но ответа не получил. Так что я предполагаю, что наша проверка никак не связана с исправлениями, которые мы дальше рассмотрим. Разработчикам пришлось искать их другим путём. А могли бы просто взять и запустить PVS-Studio :). Сейчас, после публикации статей, мы в основном пишем об этом в GitHub репозиторий проекта.


Мне стало интересно, были ли исправлены ошибки, описанные в предыдущей статье, или всё так и осталось. Большинство из найденных ошибок действительно были исправлены. Это показывает, что отобранные предупреждения анализатора оказались верными. Хотя для написания статьи с отчётом работал человек, не участвующий в разработке XNU, то есть близко не знакомый с этим исходным кодом.


Я приведу здесь несколько примеров исправлений. Но, чтобы не раздувать объём статьи, не буду полностью приводить объяснение ошибок. Если из исправления будет неясно, в чём была проблема, то вы всегда можете обратиться к первой статье по проверке этого проекта. Я не буду разбирать все исправленные фрагменты, большинство из фрагментов всё-таки было поправлено. А фрагментов в предыдущей статье было ни много ни мало 64!


Перейдём к рассмотрению исправлений примеров из прошлой статьи.


Фрагмент N1, в котором член класса сравнивался сам с собой:


intkey_parse(      struct mbuf *m,      struct socket *so){  ....  if ((m->m_flags & M_PKTHDR) == 0 ||      m->m_pkthdr.len != m->m_pkthdr.len) {    ....    goto senderror;  }  ....}

Был исправлен следующим образом:


0818_XNU_MacOS_Kernel_ru/image2.png


Где макрос, из которого получена переменная orglen, выглядит следующим образом:


#define PFKEY_UNUNIT64(a) ((a) << 3)

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


Еще один пример исправления, который я хочу привести здесь, фрагмент N5, где знак равно всё-таки был исправлен на проверку на равенство.


0818_XNU_MacOS_Kernel_ru/image3.png


Накосячить в условии assertf одно, но ещё и перезаписать переменную для отладочной версии такое точно стоит поправить.


Фрагменты 6 и 7 были исправлены одинаково. Оказалось, что во вложенной проверке перепутали значение перечислителя для сравнения. Вместо PBUF_TYPE_MBUF во внутренней проверке должен быть элемент PBUF_TYPE_MEMORY в обоих случаях.


0818_XNU_MacOS_Kernel_ru/image4.png


В случае фрагментов N8, 9, 10 исправление было таким:


0818_XNU_MacOS_Kernel_ru/image5.png


На это исправление я обратила внимание, так как серьёзная часть коммита в целом (обновление репозитория до xnu-4903.270.47 от 11 января) содержит помимо прочего много правок код-стайла. Это может указывать на то, что для данной версии кодовая база была подчищена с помощью разных инструментов качества кода. Что сделает эту проверку PVS-Studio более интересной. Ведь видно, что качество кодовой базы уже было улучшено другими инструментами.


Что касается фрагментов 11, 12, 13, 14 был исправлен только фрагмент 11:


0818_XNU_MacOS_Kernel_ru/image6.png


Остальные остались прежними. Похоже, кто-то невнимательно прочитал наш отчёт ;) (или отчёт анализатора, использованный для улучшения качества кода в этом коммите). Приведу здесь код, на который было выдано одно из предупреждений, чтобы не было сомнений в аналогичности ошибки:


static intkauth_resolver_getwork(user_addr_t message){  struct kauth_resolver_work *workp;  int error;  KAUTH_RESOLVER_LOCK();  error = 0;  while ((workp = TAILQ_FIRST(....)) == NULL) { // <=    thread_t thread = current_thread();    struct uthread *ut = get_bsdthread_info(thread);    ut->uu_save.uus_kauth.message = message;    error = msleep0(....);    KAUTH_RESOLVER_UNLOCK();    /*     * If this is a wakeup from another thread in the resolver     * deregistering it, error out the request-for-work thread     */    if (!kauth_resolver_identity) {      printf("external resolver died");      error = KAUTH_RESOLVER_FAILED_ERRCODE;    }    return error; //<=  }  return kauth_resolver_getwork2(message);}

Предупреждение PVS-Studio: V612 An unconditional 'return' within a loop. kern_credential.c 951


Я привела код почти целиком, чтобы можно было сформировать общее представление о том, что происходит в этой функции. В случае отмеченного цикла при выполнении условия входа в него будет совершён один проход по телу цикла, завершающийся возвращением error. Видимо, подразумевалось, что если выполняется условие (workp = TAILQ_FIRST(....)) == NULL, то нужно найти причину ошибки и завершить функцию возвращением информации об ошибке. Однако по какой-то причине вместо if был написан while, как и во фрагменте из предыдущей статьи. Строчка error = msleep0(....) выглядит в коде таким образом:


error = msleep0(&kauth_resolver_unsubmitted,                kauth_resolver_mtx,                PCATCH,                "GRGetWork",                0,                 kauth_resolver_getwork_continue);

Здесь последним аргументом передаётся указатель на функцию kauth_resolver_getwork_continue. В теле этой функции есть условие, аналогичное условию цикла, на который нам указал анализатор. Но в нём уже корректно используется if, а не while.


static intkauth_resolver_getwork_continue(int result){  ....  if (TAILQ_FIRST(&kauth_resolver_unsubmitted) == NULL) {    ....    return error;  }  ....}

В принципе этот код работает немного сложнее, чем я описала. В нём присутствует рекурсия (в методе kauth_resolver_getwork_continue), и, как я поняла, он нацелен на нахождение потоков, которые можно перезагрузить. Но я не стала вдаваться в подробности, так как while всё равно лишний. Возможно, он остался здесь с того времени, когда исходный код выполнял ту же задачу, но без использования рекурсии.


Это примеры из начала статьи. Проскочим в середину и возьмём фрагмент N40. В нём одному и тому же элементу дважды присваивается одно значение:


Предупреждение PVS-Studio: V519 CWE-563 The 'wrap.Seal_Alg[0]' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 2070, 2071. gss_krb5_mech.c 2071


Эта ошибка, конечно же, тоже была поправлена:


0818_XNU_MacOS_Kernel_ru/image7.png


Ну и ближе к концу статьи, фрагмент 62 был исправлен так, как и было предложено в предыдущей статье. Причём это было единственной правкой в том файле.


0818_XNU_MacOS_Kernel_ru/image8.png


Фрагменты 63 и 64 также были исправлены, но там код был изменён капитально. Поэтому понять, какое исправление было именно для рассмотренного предупреждения, сложно.


Новые находки


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


К этой проверке cloc насчитал в проекте 1346 *.c файлов, 1822 С/C++ хэдера и 225 *.cpp файлов.


Ну и перейдём к разбору интересных находок.


Фрагмент N1


voidpe_identify_machine(__unused boot_args *args){  ....  // Start with default values.  gPEClockFrequencyInfo.timebase_frequency_hz = 1000000000;  gPEClockFrequencyInfo.bus_frequency_hz      =  100000000;  ....  gPEClockFrequencyInfo.dec_clock_rate_hz =     gPEClockFrequencyInfo.timebase_frequency_hz;  gPEClockFrequencyInfo.bus_clock_rate_hz =   gPEClockFrequencyInfo.bus_frequency_hz;  ....   gPEClockFrequencyInfo.bus_to_dec_rate_den =    gPEClockFrequencyInfo.bus_clock_rate_hz /    gPEClockFrequencyInfo.dec_clock_rate_hz;}

Предупреждение PVS-Studio: V1064 The 'gPEClockFrequencyInfo.bus_clock_rate_hz' operand of integer division is less than the 'gPEClockFrequencyInfo.dec_clock_rate_hz' one. The result will always be zero. pe_identify_machine.c 72


Все используемые здесь поля имеют целочисленный тип:


extern clock_frequency_info_t gPEClockFrequencyInfo;struct clock_frequency_info_t {  unsigned long bus_clock_rate_hz;  unsigned long dec_clock_rate_hz;  unsigned long bus_to_dec_rate_den;  unsigned long long bus_frequency_hz;  unsigned long timebase_frequency_hz;  ....};

Через промежуточные присвоения полю gPEClockFrequencyInfo.bus_clock_rate_hz, являющемуся делимым, присваивается значение 100000000, а полю-делителю gPEClockFrequencyInfo.dec_clock_rate_hz присваивается значение 1000000000. Делитель в этом случае в десять раз больше делимого. Так как все поля здесь являются целочисленными, поле gPEClockFrequencyInfo.bus_to_dec_rate_den окажется равным 0.


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


Фрагмент N2


voidsdt_early_init( void ){  ....  if (MH_MAGIC_KERNEL != _mh_execute_header.magic) {  ....  } else {    ....    for (....) {    const char *funcname;    unsigned long best;                           //<=    ....    funcname = "<unknown>";    for (i = 0; i < orig_st->nsyms; i++) {      char *jname = strings + sym[i].n_un.n_strx;      ....      if ((unsigned long)sym[i].n_value > best) { //<=        best = (unsigned long)sym[i].n_value;        funcname = jname;      }    }    .....  }}

Предупреждение PVS-Studio: V614 Uninitialized variable 'best' used. sdt.c 572


Насколько я поняла, этот метод ищет название некоей функции. В алгоритме используется переменная best, возможно, это положение наилучшего кандидата на результат. Однако изначально эта переменная только объявляется без инициализации. Следующее же использование сверяет значение некоего элемента с переменной best, которая будет неинициализированной на тот момент. Еще страннее, что она инициализируется только внутри условия, в котором используется её же значение.


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


Фрагмент N3


intcdevsw_isfree(int index){  struct cdevsw * devsw;  if (index < 0) {    if (index == -1) {      index = 0;    } else {      index = -index;     }    devsw = &cdevsw[index];    for (; index < nchrdev; index++, devsw++) {      if (memcmp(....) == 0) {        break;      }    }  }  if (index < 0 || index >= nchrdev) {    return -1;  }  ....  return index;}

Предупреждение PVS-Studio: V560 A part of conditional expression is always false: index < 0. bsd_stubs.c:236


Небольшой пример того, что анализатор отслеживает возможные значения переменных. В начале функции переменная index сравнивается с нулём. Если она меньше него, то во внутреннем блоке ей будет присвоено значение не меньше нуля. А следующий внешний if снова проверяет, имеет ли index значение меньше нуля, но это невозможно.


Логики программы этот момент не меняет, но есть вероятность, что подразумевалось какое-то иное условие. Ну и в любом случае лишние проверки не делают код читабельнее и понятнее.


Фрагмент N4


intnfs_vinvalbuf_internal(....){  struct nfsbuf *bp;  ....  off_t end = ....;  /* check for any dirty data before the EOF */  if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end))  {    /* clip dirty range to EOF */    if (bp->nb_dirtyend > end)    {      bp->nb_dirtyend = end;      if (bp->nb_dirtyoff >= bp->nb_dirtyend)             //<=      {        bp->nb_dirtyoff = bp->nb_dirtyend = 0;      }    }    if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) //<=    {      ....    }  }  ....}

Предупреждения PVS-Studio:


  • V547 Expression 'bp->nb_dirtyoff >= bp->nb_dirtyend' is always false. nfs_bio.c 3858
  • V560 A part of conditional expression is always true: (bp->nb_dirtyoff < end). nfs_bio.c 3862

В случае этого фрагмента к анализатору точно стоит прислушаться и попытаться упростить код. Учтите, что тут он хотя бы приведён с сокращениями.


Начнём с первого предупреждения. Анализатор решил, что nb_dirtyoff не может быть больше или равен nb_dirtyend. Разберёмся почему. Перед подозрительной проверкой есть ещё два if с проверками (bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end) и bp->nb_dirtyend > end. А также осуществляется присвоение bp->nb_dirtyend = end.


Почему же третья проверка bp->nb_dirtyoff >= bp->nb_dirtyend будет всегда false?


0818_XNU_MacOS_Kernel_ru/image9.png


Всё просто. Из условий выходит, что nb_dirtyoff меньше, чем end, а nb_dirtyend равно end. В итоге nb_dirtyend точно больше, чем nb_dirtyoff. Присвоение bp->nb_dirtyoff = bp->nb_dirtyend = 0 никогда не будет выполнено.


В итоге вот такой участок кода:


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) {  /* clip dirty range to EOF */  if (bp->nb_dirtyend > end) {    bp->nb_dirtyend = end;    if (bp->nb_dirtyoff >= bp->nb_dirtyend) {  //<=      bp->nb_dirtyoff = bp->nb_dirtyend = 0;    }  }}

Можно упростить хотя бы до такого:


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) {  if (bp->nb_dirtyend > end) {    bp->nb_dirtyend = end;  }}

Но только если в настоящий момент этот алгоритм работает корректно.


Второе предупреждение указывает на четвёртый if, вложенный в первый.


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end))

Здесь анализатор выдаёт предупреждение на основании того, что присвоение нуля никогда не будет выполнено. В итоге во внешнем условии уже была проверка bp->nb_dirtyoff < end и внутренняя проверка из-за ошибки в условии выше становится бессмысленной.


Фрагмент N5


tcp_output(struct tcpcb *tp){  ....  if (isipv6) {    ....    if (len + optlen) {      ....    }  } else {    ....    if (len + optlen) {      ....    }  }  ....}

Предупреждение PVS-Studio: V793 It is odd that the result of the 'len + optlen' statement is a part of the condition. Perhaps, this statement should have been compared with something else.


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


Конечно, может быть, что так и задумано, но чуть выше в коде есть вот такая проверка:


if (len + optlen + ipoptlen > tp->t_maxopd) {  ....}

Это наводит на мысль, что, скорее всего, в двух if'ах, на которые указал анализатор, также должно было проводиться сравнение.


Ещё замечу, что эта функция, сокращённая тут до 16 строк, занимает в оригинале 2268 строк! Ещё один возможный повод для рефакторинга ;)


Второе предупреждение на этот же участок:


V793 It is odd that the result of the 'len + optlen' statement is a part of the condition. Perhaps, this statement should have been compared with something else.


Фрагмент N6


intttyinput(int c, struct tty *tp){  ....  if (tp->t_rawq.c_cc + tp->t_canq.c_cc) {  ....}

Предупреждение PVS-Studio: V793 It is odd that the result of the 'tp->t_rawq.c_cc + tp->t_canq.c_cc' statement is a part of the condition. Perhaps, this statement should have been compared with something else. tty.c 568


Аналогичный случай. Тут повыше в коде снова есть проверка, которая не просто использует сумму, а сравнивает результат с другой переменной:


if (   tp->t_rawq.c_cc + tp->t_canq.c_cc > I_HIGH_WATER  3 // <=    && ....) {  ....}

В упрощённом коде условие, на которое указал анализатор, выглядит заметно. Но в оригинале оно было вложено в несколько if. Так что при код-ревью такое можно и пропустить, а анализатор не пропустит ;)


Фрагмент N7


errno_tmbuf_adjustlen(mbuf_t m, int amount){  /* Verify m_len will be valid after adding amount */  if (amount > 0) {    int used =  (size_t)mbuf_data(m)              - (size_t)mbuf_datastart(m)              + m->m_len;    if ((size_t)(amount + used) > mbuf_maxlen(m)) {      ....    }  ....  return 0;}

Предупреждение PVS-Studio: V1028 Possible overflow. Consider casting operands of the 'amount + used' operator to the 'size_t' type, not the result. kpi_mbuf.c


Снова ошибка в условии, но уже совсем другого рода. Вместо приведения к size_t операндов сложения, чтобы результат точно поместился в числовой тип, к size_t приводится результат сложения. Если в итоге сложения возникнет переполнение, то с результатом mbuf_maxlen(m) будет сравниваться бессмысленное значение, приведённое к size_t. Раз программист всё-таки хотел защититься от переполнения, то стоит его сделать правильно:


if ((size_t)amount + used > mbuf_maxlen(m))

Таких срабатываний было несколько, стоит обратить на этот момент внимание.


  • V1028 Possible overflow. Consider casting operands, not the result. vm_compressor_pager.c 1165
  • V1028 Possible overflow. Consider casting operands, not the result. vm_compressor_pager.c 1131
  • V1028 Possible overflow. Consider casting operands, not the result. audit_worker.c 241
  • V1028 Possible overflow. Consider casting operands of the '((u_int32_t) slp * hz) + 999999' operator to the 'long' type, not the result. tty.c 2199

Фрагмент N8


intfdavail(proc_t p, int n){  ....  char *flags;  int i;  int lim;  ....  lim = (int)MIN(....);  if ((i = lim - fdp->fd_nfiles) > 0 && (n -= i) <= 0) //<=  {    return 1;  }  ....  for (....)  {    if (*fpp == NULL && !(*flags & UF_RESERVED) && --n <= 0)    {      return 1;    }  }  return 0;}

Предупреждение PVS-Studio: V1019 Compound assignment expression 'n -= i' is used inside condition. kern_descrip.c_99 3916


Этот код, на мой взгляд, является крайне сложночитаемым. Возможно, условие, на которое указал анализатор, стоит переписать в более простом виде:


i = lim - fdp->fd_nfiles;if (i > 0){  n -= i;  if(n <= 0)    return 1;}

Этот код выглядит менее эффективным, но точно является более понятным. Для быстрой проверки равнозначности эффективности этого кода можно зайти на Godbolt (Compiler Explorer), где, кстати, можно тестировать работу диагностик PVS-Studio. Анализатор легко найти среди инструментов этого сервиса.


Если не включать оптимизации, то ассемблерный код получится на пару строк больше. А вот с оптимизациями разницы уже нет никакой. Так что писать тут хитрый код нет смысла, компилятор всё сам сделает, как надо.


Но, если обратить внимание на тело этого if, новое значение n в нём не используется. То есть вполне возможно, что никакое присвоение здесь и не нужно. Тогда можно обойтись таким кодом:


i = lim - fdp->fd_nfiles;if (i > 0) {  if(n  i <= 0)    return 1;}

И, более того, исходный код может приводить к ошибке при дальнейшем использовании переменной n. Если выражение (n -= i) <= 0 окажется ложным, то далее будет использоваться уже новое значение n. Так как я не работала вплотную с исходным кодом, мне сложно сказать, какое поведение является верным.


Фрагмент N9


static errno_tvsock_put_message_listening(struct vsockpcb *pcb,                             enum vsock_operation op,                            struct vsock_address src,                             struct vsock_address dst){  switch (op)  {    case VSOCK_REQUEST:      ....      if (....)      {        vsock_pcb_safe_reset_address(pcb, dst, src);        ....      }      ....      done:        ....        break;    case VSOCK_RESET:      error = vsock_pcb_safe_reset_address(pcb, dst, src);      break;    default:      vsock_pcb_safe_reset_address(pcb, dst, src);      ....      break;  }  return error;}

Предупреждение PVS-Studio: V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 549


Этот момент запросто может оказаться и не ошибкой. Но крайне подозрительно, что сигнатура вызываемой в этом фрагменте функции выглядит так:


static errno_tvsock_pcb_safe_reset_address(struct vsockpcb *pcb,                              struct vsock_address src,                              struct vsock_address dst)

При использовании этой функции в данном фрагменте последние два аргумента с аналогичными именами передаются в другом порядке.


Срабатывания на тот же фрагмент:


  • V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 587
  • V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 590

Фрагмент N10


intifclassq_tbr_set(struct ifclassq *ifq, ....){  struct tb_regulator *tbr;  ....  tbr = &ifq->ifcq_tbr;  ....  tbr->tbr_rate = TBR_SCALE(rate / 8) / machclk_freq;  ....  tbr->tbr_last = read_machclk();  if (   tbr->tbr_rate > 0               //<=      && (ifp->if_flags & IFF_UP))  {     ....  } else {    ....  }  ....  return 0;}

Предупреждение PVS-Studio: V1051 Consider checking for misprints. It's possible that the 'tbr->tbr_last' should be checked here. classq_subr.c 685


В проекте эта диагностика работала не лучшим образом, так как в коде постоянно над телом условия или цикла инициализировались сторонние переменные с именами, похожими на используемые в условии. Поэтому на этот раз диагностика выдала несколько явно ложных предупреждений. Но рассматриваемое нами срабатывание всё же показалось мне подозрительным, так как проверяемое поле tbr_rate не использовалось в теле условия и было инициализировано на 35 строк выше этой проверки. А вот поле tbr_last, инициализированное прямо перед этой проверкой, больше нигде не используется. Можно предположить, что проверить нужно было его вместо tbr_rate.


Фрагмент N11


voidaudit_arg_mac_string(struct kaudit_record *ar, ....){  if (ar->k_ar.ar_arg_mac_string == NULL)  {    ar->k_ar.ar_arg_mac_string = kheap_alloc(....);  }  ....  if (ar->k_ar.ar_arg_mac_string == NULL)  {    if (ar->k_ar.ar_arg_mac_string == NULL) // <=    {      return;    }  }  ....}

Предупреждение PVS-Studio: V571 Recurring check. The 'if (ar->k_ar.ar_arg_mac_string == NULL)' condition was already verified in line 245. audit_mac.c 246


Предупреждение PVS-Studio: V547 Expression 'ar->k_ar.ar_arg_mac_string == NULL' is always true. audit_mac.c 246


На этот код анализатор выдал сразу два предупреждения.


Сначала взгляд может зацепиться за то, что проверка в самом первом if и во втором совпадает. Но тут всё правильно: внутри тела первой проверки аллоцируется память, а для второй проверки есть пояснение:


/* * XXX This should be a rare event. * If kheap_alloc() returns NULL, * the system is low on kernel virtual memory. To be * consistent with the rest of audit, just return * (may need to panic if required to for audit). */

Судя по этому комментарию, во второй проверке не должно быть никакой внутренней проверки. Нужно просто выйти из метода. Так что, скорее всего, внутренняя проверка была продублирована случайно и не имеет никакого смысла.


Хотя возможен и тот вариант, что во внутренней проверке нужно было проверить какое-то другое поле. Но сюда закралась ошибка копипасты, и разработчик забыл поправить имя поля.


Фрагмент N12


intutf8_encodestr(....){  u_int16_t ucs_ch;  int swapbytes = ....;  ....  ucs_ch = swapbytes ? OSSwapInt16(*ucsp++) : *ucsp++;  ....}

Предупреждение PVS-Studio: V567 Undefined behavior. The 'ucsp' variable is modified while being used twice between sequence points. vfs_utfconv.c 298


Макросы очень коварная штука. Возможно, вы даже уже встречались с нашей статьей "Вред макросов для C++ кода". Я обычно при написании статей избегаю срабатываний на макросы. С ними всегда всё оказывается сложно без знакомства с кодовой базой проекта.


Но в случае этой ошибки всё оказалось чуть проще. Хотя, чтобы дойти до причины и развернуть цепочку макросов, пришлось прыгнуть в ту ещё кроличью нору. Собственно, цепочка эта начинается с выражения OSSwapInt16(*ucsp++).


0818_XNU_MacOS_Kernel_ru/image10.png


Потом я поняла, что есть способ попроще, и просто обратилась к .i файлу, который остался после проверки проекта. По нему строка с этим макросом развернулась следующим образом:


ucs_ch = swapbytes? ( (__uint16_t)(__builtin_constant_p(*ucsp++)   ? ((__uint16_t)(  (((__uint16_t)(*ucsp++) & 0xff00U) >> 8)                   | (((__uint16_t)(*ucsp++) & 0x00ffU) << 8)))   : _OSSwapInt16(*ucsp++))): *ucsp++;

Больше всего здесь нас интересует вот этот участок выражения:


  (((__uint16_t)(*ucsp++) & 0xff00U) >> 8)| (((__uint16_t)(*ucsp++) & 0x00ffU) << 8)

Никакой из операторов в выражении не является точкой следования. Так как точно неизвестно, какой из аргументов оператора | будет вычисляться первым, значение *uscp оказывается неопределённым.


Для диагностики V567 PVS-Studio предоставляет крайне подробную документацию. Если вам интересно, почему такой код может приводить к неопределённому поведению, документация может стать хорошим началом изучения этой проблемы.


Однако это ещё не всё! Есть очень интересный и важный момент. Готова поспорить, что человек, писавший этот код, планировал увеличить значение *ucsp только один раз. Но, на самом деле, значение увеличится дважды. Это не видно и непонятно. Макросы очень и очень опасны из-за вот таких случаев. Во многих ситуациях лучше написать обыкновенную функцию. Скорее всего, компилятор автоматически выполнит подстановку и никакого ухудшения производительности не произойдёт.


Фрагмент N13


struct pf_status pf_status;intpf_insert_state(struct pf_state *s, ....){  ....  if (....) {    s->id = htobe64(pf_status.stateid++);    ....  }  ....}

Предупреждение PVS-Studio: V567 Undefined behavior. The 'pf_status.stateid' variable is modified while being used twice between sequence points. pf.c 1440


И снова коварные макросы смешали все карты для инкремента. Рассмотрим строку с вызовом htobe64, которая оказалась подозрительной для анализатора после препроцессинга:


s->id = (__builtin_constant_p(pf_status.stateid++) ? ((__uint64_t)((((__uint64_t)(pf_status.stateid++) &0xff00000000000000ULL) >> 56) | (((__uint64_t)(pf_status.stateid++) &0x00ff000000000000ULL) >> 40) | (((__uint64_t)(pf_status.stateid++) &0x0000ff0000000000ULL) >> 24) | (((__uint64_t)(pf_status.stateid++) &0x000000ff00000000ULL) >> 8)  | (((__uint64_t)(pf_status.stateid++) &0x00000000ff000000ULL) << 8)  | (((__uint64_t)(pf_status.stateid++) &0x0000000000ff0000ULL) << 24) | (((__uint64_t)(pf_status.stateid++) &0x000000000000ff00ULL) << 40) | (((__uint64_t)(pf_status.stateid++) &0x00000000000000ffULL) << 56))) : _OSSwapInt64(pf_status.stateid++));

0818_XNU_MacOS_Kernel_ru/image11.png


Проблема собственно та же, что и в предыдущем примере. Во внутренней цепочке с операндами | и & нет точек следования. Поэтому неизвестно, какое значение примет pf_status.stateid на моменте выполнения каждой операции. Результат также неопределён.


И, опять-таки, переменная увеличивается несколько раз подряд, что является неприятным сюрпризом от макроса :).


Вот остальные срабатывания этой диагностики на этом проекте:


  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. ip_id.c 186
  • V567 Undefined behavior. The 'lp' variable is modified while being used twice between sequence points. nfs_boot.c 505
  • V567 Undefined behavior. The 'lp' variable is modified while being used twice between sequence points. nfs_boot.c 497
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 588
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 665
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 1543

Фрагмент N14


__private_extern__ boolean_tipsec_send_natt_keepalive(....){  ....  struct udphdr *uh = (__typeof__(uh))(void *)(  (char *)m_mtod(m)                                                + sizeof(*ip));  ....  if (....)  {    uh->uh_sport = (u_short)sav->natt_encapsulated_src_port;  } else {    uh->uh_sport = htons((u_short)esp_udp_encap_port);  }  uh->uh_sport = htons((u_short)esp_udp_encap_port);  ....}

Предупреждение PVS-Studio: V519 The 'uh->uh_sport' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 4866, 4870. ipsec.c 4870


В этом фрагменте возникла подозрительная ситуация: полю uh_sport в зависимости от определённого условия присваиваются разные значения. Однако сразу после if-else этому же полю снова присваивается значение, такое же как в ветке else. В итоге этот if-else блок теряет смысл, так как значение поля всё равно будет перезаписано.


Фрагмент N15


static kern_return_tvm_shared_region_slide_page_v3(vm_offset_t vaddr, ....){  ....  uint8_t *page_content = (uint8_t *)vaddr;  uint16_t page_entry;  ....  uint8_t* rebaseLocation = page_content;  uint64_t delta = page_entry;  do {    rebaseLocation += delta;    uint64_t value;    memcpy(&value, rebaseLocation, sizeof(value));    ....    bool isBind = (value & (1ULL << 62)) == 1;   // <=    if (isBind) {      return KERN_FAILURE;    }    ....  } while (delta != 0);  ....}

Предупреждение PVS-Studio: V547 Expression '(value & (1ULL << 62)) == 1' is always false. vm_shared_region.c 2820


Тут получилось много кода из-за использования большого числа переменных. Но интересует нас только указанная мною строка с инициализацией isBind. Разберём это выражение по шагам.


В результате побитового сдвига создаётся маска с единственной единицей в 63-ем бите. Результат побитового & с переменной value может принимать только значения 0 или 0x4000000000000000. А никакое из этих значений не равно 1. В итоге условие всегда будет ложным.


Судя по тому, что выполнение этого условия должно было привести к выходу из функции с возвратом KERN_FAILURE, возможно, как раз значение 0x4000000000000000 является тем самым исключительным случаем, после которого нужно выйти из функции. Тогда результат побитовых операций надо было сравнивать с этим числом, а не с 1. Ну, или написать так:


bool isBind = (value & (1ULL << 62)) != 0;

Фрагмент N16


intvn_path_package_check(char *path, int pathlen, ....){  char *ptr, *end;  int comp = 0;  ....  end = path + 1;  while (end < path + pathlen && *end != '\0') {    while (end < path + pathlen && *end == '/' && *end != '\0') {      end++;    }    ptr = end;    while (end < path + pathlen && *end != '/' && *end != '\0') {      end++;    }    ....  }  ....}

Предупреждение PVS-Studio: V590 Consider inspecting this expression. The expression is excessive or contains a misprint. vfs_subr.c 3589


Эта диагностика всегда указывает на излишний код. Иногда под ним скрывается более серьёзная ошибка. Но здесь это, скорее всего, просто недочёт. Предупреждение было выдано на первый внутренний while. Нет смысла проверять, что символ одновременно равен '/' и не равен '\0'. Достаточно только первой проверки, так как если *end равен '/', то он точно не может быть равен '\0'.


Следующий while содержит столько же проверок. Но из-за того, что в обоих случаях проверяется неравенство, эти проверки могут работать вместе. Возможно, сначала был написан второй while, а первый был скопирован с него с изменённой проверкой для '/'. Тогда перед нами недочёт, возникший из-за копипасты.


Заключение


В этот раз в проекте нашлось несколько меньше ошибок, чем в предыдущей статье. Весьма вероятно, что в процесс разработки XNU был внедрён статический анализ и другие инструменты контроля качества кода. Почти наверняка на проекте используется Clang Static Analyzer. Но ошибки и недочёты всё-таки нашлись. Я не стала приводить здесь некоторые срабатывания на подозрительные места, вывод по которым можно сделать только на основании большего понимания кодовой базы.


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


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


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Victoria Khanieva. MacOS Kernel, Is This Apple Rotten?.

Подробнее..

Опыт команды PVS-Studio повышение производительности C анализатора на Windows при переходе на Clang

31.05.2021 18:12:13 | Автор: admin

С самого своего начала C++ анализатор PVS-Studio для Windows (тогда еще Viva64 версии 1.00 в 2006 году) собирался компилятором MSVC. С выходом новых релизов C++ ядро анализатора научилось работать на Linux и macOS, и структура проекта была переведена на использование CMake. Но под Windows сборка по-прежнему происходила с помощью компилятора MSVC. 29 апреля 2019 года разработчики Visual Studio объявили о включении в свою среду разработки набора утилит LLVM и компилятора Clang. И сейчас у нас наконец дошли руки, чтобы попробовать его в действии.

Тестирование производительности

В качестве бенчмарка воспользуемся нашей утилитой для регрессионного тестирования анализатора под названием SelfTester. Суть её работы заключается в анализе набора разных проектов и сравнении результатов анализа с эталонными. Например, если при каких-то правках в ядре анализатора появились ложные предупреждения или пропали правильные, значит появилась регрессия, которую надо исправить. Более подробно про SelfTester можно прочитать в статье "Лучшее враг хорошего".

Среди тестовой базы достаточно разнообразные по объёму кода проекты. Как правило, если рабочий компьютер или тестовый сервер не нагружен, то время тестирования SelfTester'ом на одной и той же версии ядра варьируется в пределах погрешности. В случае если производительность анализатора не уберегли, это значительно скажется на общем времени тестирования.

После того как сборка C++ ядра перешла на Clang, SelfTester стал проходить на 11 минут быстрее.

Выигрыш по производительности в 13% это довольно заметно, учитывая, что достаточно просто поменять компилятор, не так ли?

Минусы тоже есть, но незначительные. Сборка дистрибутива замедлилась на 8 минут, а размер исполняемого файла подрос на 1,6 Мбайт (из них ~500 Кбайт из-за статической линковки рантайма).

Видимо, производительность достигается более долгим этапом LTO (большую часть сборки занимает именно линковка) и более агрессивным раскручиванием циклов и встраиванием функций.

Далее хочется поделиться подводными камнями, возникшими в процессе перехода.

Генерация сборки под Clang

Скрипты CMake позволяют нам собирать свой код всеми мейнстримными компиляторами под нужные операционные системы.

Прежде всего необходимо установить компоненты компилятора Clang через Visual Studio Installer.

Clang-cl это так называемый "драйвер", который позволяет использовать clang с параметрами от cl.exe. Таким образом, он должен прозрачно взаимодействовать с MSBuild, практически как родной компилятор.

Также можно воспользоваться официальными сборками от проекта LLVM, которые можно найти на их репозитории GitHub. Однако для них нужно установить дополнительный плагин, чтобы Visual Studio смогла найти компиляторы. Имя toolset'а будет llvm, а не clangcl, как показано дальше в примерах.

Указываем toolchain в команде генерации solution для Visual Studio:

cmake -G "Visual Studio 16 2019" -Tclangcl <src>

Либо используем GUI:

Открываем получившийся проект, собираем. И, конечно же, получаем пачку ошибок.

Чиним сборку

Хоть сlang-cl внешне и ведет себя как CL, под капотом это совсем другой компилятор, со своими приколами.

Мы стараемся не игнорировать предупреждения компиляторов, поэтому используем флаги /W4 и /WX. Однако Clang может генерировать дополнительные предупреждения, которые сейчас мешают сборке. Пока выключим их:

if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")  ....  if (WIN32)    add_compile_options(-Wno-error=deprecated-declarations                        -Wno-error=reorder-ctor                        -Wno-error=format-security                        -Wno-error=macro-redefined                        -Wno-error=bitwise-op-parentheses                        -Wno-error=missing-field-initializers                        -Wno-error=overloaded-virtual                        -Wno-error=invalid-source-encoding                        -Wno-error=multichar                        -Wno-unused-local-typedef                        -Wno-c++11-narrowing)  ....  endif()endif()

Немного получше.

Компиляторы GCC и Clang имеют встроенную поддержку типа int128, в отличие от MSVC под Windows. Поэтому в своё время была написана обертка с реализацией Int128 для Windows (на ассемблерных вставках и обернутая ifdef'ами, в лучших традициях C/C++). Поправим определения для препроцессора, заменив:

if (MSVC)  set(DEFAULT_INT128_ASM ON)else ()  set(DEFAULT_INT128_ASM OFF)endif ()

на

if (MSVC AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")  set(DEFAULT_INT128_ASM ON)else ()  set(DEFAULT_INT128_ASM OFF)endif ()

Обычно библиотеку с builtin'ами линкеру (lld) передает драйвер компилятора, будь то clang.exe или clang-cl.exe. Но в данном случае линкером заправляет MSBuild напрямую, который не знает, что нужно её использовать. Соответственно, драйвер никак не может передать флаги линкеру, поэтому приходится разбираться самим.

if (CMAKE_GENERATOR MATCHES "Visual Studio")  link_libraries("$(LLVMInstallDir)\\lib\\clang\\\${CMAKE_CXX_COMPILER_VERSION}\\lib\\windows\\\clang_rt.builtins-x86_64.lib")else()  link_libraries(clang_rt.builtins-x86_64)endif()

Ура! Сборка заработала. Однако дальше при запуске тестов нас ждала куча ошибок сегментации:

Отладчик при этом показывает какое-то странное значение в IntegerInterval, а на самом деле проблема находится немного дальше:

В разных структурах для Dataflow-механизма активно применяется ранее упомянутый тип Int128, а для работы с ним используются SIMD-инструкции. И падение вызвано невыровненным адресом:

Инструкция MOVAPS перемещает из памяти набор чисел с плавающей запятой в регистры для SIMD-операций. Адрес при этом обязан быть выровнен, в его конце должен стоять 0, а оказалась 8. Придется помочь компилятору, задав правильное выравнивание:

class alignas(16) Int128

Порядок.

Последняя проблема вылезла из-за Docker-контейнеров:

Сборка под MSVC всегда делалась со статической линковкой рантайма, а для экспериментов с Clang рантайм переключили на динамический. Оказалось, что в образах с Windows по умолчанию не установлены Microsoft Visual C++ Redistributable. Решили вернуть статическую линковку, чтобы у пользователей не возникало таких же неприятностей.

Заключение

Несмотря на то, что пришлось немного повозиться с подготовкой проекта, мы остались довольны ростом производительности анализатора более чем на 10%.

Последующие релизы PVS-Studio на Windows будут собираться с помощью компилятора Clang.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Govorov, Sergey Larin. PVS-Studio Team: Switching to Clang Improved PVS-Studio C++ Analyzer's Performance.

Подробнее..

Должен ли out-параметр быть проинициализирован до возврата из метода?

13.02.2021 00:15:04 | Автор: admin

0800_OutParamsCs_ru/image1.png


Наверняка каждый, кто писал на C#, сталкивался с использованием out-параметров. Кажется, что с ними всё предельно просто и понятно. Но так ли это на самом деле? Для затравки предлагаю начать с задачки для самопроверки.


Напомню, что out-параметры должны быть проинициализированы вызываемым методом до выхода из него.


А теперь посмотрите на следующий фрагмент кода и ответьте, компилируется ли он.


void CheckYourself(out MyStruct obj){  // Do nothing}

MyStruct какой-то значимый тип:


public struct MyStruct{ .... }

Если вы уверенно ответили 'да' или 'нет' приглашаю к дальнейшему прочтению, так как всё не так однозначно...


Предыстория


Начнём с небольшой предыстории. Как мы вообще погрузились в изучение out-параметров?


Всё началось с разработки очередного диагностического правила для PVS-Studio. Идея диагностики заключается в следующем один из параметров метода имеет тип CancellationToken. При этом данный параметр в теле метода не используется. Как следствие, программа может не реагировать (или реагировать несвоевременно) на какие-то действия отмены, например, отмены операции со стороны пользователя. В ходе просмотра срабатываний одной из первых версий диагностики нашли код примерно следующего вида:


void Foo(out CancellationToken ct, ....){  ....  if (flag)    ct = someValue;  else    ct = otherValue;  ....}

Очевидно, что это было false positive срабатыванием, поэтому я попросил коллегу добавить в набор модульных тестов ещё один, "с out параметрами". Он добавил тестов, в том числе тест такого вида:


void TestN(out CancellationToken ct){  Console.WriteLine("....");}

В первую очередь меня интересовали тесты с инициализаций параметров, но я повнимательнее присмотрелся к этому И тут меня осенило! А как этот код, собственно, компилируется? И компилируется ли вообще? Код компилировался. Тут я понял, что намечается статья. :)


Ради эксперимента решили поменять CancellationToken на какой-нибудь другой значимый тип. Например, TimeSpan:


void TestN(out TimeSpan timeSpan){  Console.WriteLine("....");}

Не компилируется. Что ж, ожидаемо. Но почему компилируется пример с CancellationToken?


Модификатор параметра out


Давайте вновь вспомним, что за модификатор параметра такой out. Вот основные тезисы, взятые с docs.microsoft.com (out parameter modifier):


  • The out keyword causes arguments to be passed by reference;
  • Variables passed as out arguments do not have to be initialized before being passed in a method call. However, the called method is required to assign a value before the method returns.

Особо прошу обратить внимание на выделенное предложение.


Внимание вопрос. В чём отличие следующих трёх методов, и почему последний компилируется, а первый и второй нет?


void Method1(out String obj) // compilation error{ }void Method2(out TimeSpan obj) // compilation error{ }void Method3(out CancellationToken obj) // no compilation error{ }

Пока закономерности не видно. Может быть есть какие-то исключения, которые описаны в доках? Для типа CancellationToken, например. Хотя это было бы немного странно что в нём такого особенного? В приведённой выше документации я никакой информации по этому поводу не нашёл. За дополнительными сведениями предлагают обращаться к спецификации языка: For more information, see the C# Language Specification. The language specification is the definitive source for C# syntax and usage.


Что ж, посмотрим спецификацию. Нас интересует раздел "Output parameters". Ничего нового всё то же самое: Every output parameter of a method must be definitely assigned before the method returns.


0800_OutParamsCs_ru/image2.png


Что ж, раз официальная документация и спецификация языка ответов нам не дали, придётся немного поковыряться в компиляторе. :)


Погружаемся в Roslyn


Исходники Roslyn можно загрузить со страницы проекта на GitHub. Для экспериментов я взял ветку master. Работать будем с решением Compilers.sln. В качестве стартового проекта для экспериментов используем csc.csproj. Можно даже его запустить на файле с нашими тестами, чтобы убедиться в воспроизводимости проблемы.


Для экспериментов возьмём следующий код:


struct MyStruct{  String _field;}void CheckYourself(out MyStruct obj){  // Do nothing}

Для проверки, что ошибка на месте, соберём и запустим компилятор на файле, содержащем этот код. И действительно ошибка на месте: error CS0177: The out parameter 'obj' must be assigned to before control leaves the current method


Кстати, это сообщение может стать неплохой отправной точкой для погружения в код. Сам код ошибки (CS0177) наверняка формируется динамически, а вот строка формата для сообщения, скорее всего, лежит где-нибудь в ресурсах. И это действительно так находим ресурс ERR_ParamUnassigned:


<data name="ERR_ParamUnassigned" xml:space="preserve">  <value>The out parameter '{0}' must be assigned to          before control leaves the current method</value></data>

По тому же имени находим код ошибки ERR_ParamUnassigned = 177, а также несколько мест использования в коде. Нас интересует место, где добавляется ошибка (метод DefiniteAssignmentPass.ReportUnassignedOutParameter):


protected virtual void ReportUnassignedOutParameter(  ParameterSymbol parameter,   SyntaxNode node,   Location location){  ....  bool reported = false;  if (parameter.IsThis)  {    ....  }  if (!reported)  {    Debug.Assert(!parameter.IsThis);    Diagnostics.Add(ErrorCode.ERR_ParamUnassigned, // <=                    location,                     parameter.Name);  }}

Что ж, очень похоже на интересующее нас место! Ставим точку останова и убеждаемся, что это нужное нам место. По результатам в Diagnostics будет записано как раз то сообщение, которое мы видели:


0800_OutParamsCs_ru/image3.png


Что ж, шикарно. А теперь поменяем MyStruct на CancellationToken, иии Мы всё также проходим эту ветку исполнения кода, в которой ошибка записывается в Diagnostics. То есть, она всё ещё на месте! Вот это поворот.


Следовательно, недостаточно отследить место, где ошибка компиляции добавляется, нужно мониторить дальше.


Немного покопавшись в коде, выходим на метод DefiniteAssignmentPass.Analyze, который инициировал запуск анализа, проверяющего, в том числе, что out-параметры инициализируются. В нём обнаруживаем, что соответствующий анализ запускается 2 раза:


// Run the strongest version of analysisDiagnosticBag strictDiagnostics = analyze(strictAnalysis: true);....// Also run the compat (weaker) version of analysis to see    if we get the same diagnostics.// If any are missing, the extra ones from the strong analysis    will be downgraded to a warning.DiagnosticBag compatDiagnostics = analyze(strictAnalysis: false);

А немного ниже находится интересное условие:


// If the compat diagnostics did not overflow and we have the same    number of diagnostics, we just report the stricter set.// It is OK if the strict analysis had an overflow here,   causing the sets to be incomparable: the reported diagnostics will// include the error reporting that fact.if (strictDiagnostics.Count == compatDiagnostics.Count){  diagnostics.AddRangeAndFree(strictDiagnostics);  compatDiagnostics.Free();  return;}

Ситуация понемногу проясняется. В результате работы и, strict, и compat анализа, когда мы пытаемся скомпилировать наш код с MyStruct, оказывается одинаковое количество диагностик, которые мы в результате и выдадим.


0800_OutParamsCs_ru/image4.png


Если же мы меняем в нашем примере MyStruct на CancellationToken, strictDiagnostics будет содержать 1 ошибку (как мы уже видели), а в compatDiagnostics не будет ничего.


0800_OutParamsCs_ru/image5.png


Как следствие, приведённое выше условие не выполняется и исполнение метода не прерывается. Куда же девается ошибка компиляции? А она понижается до предупреждения:


HashSet<Diagnostic> compatDiagnosticSet   = new HashSet<Diagnostic>(compatDiagnostics.AsEnumerable(),                             SameDiagnosticComparer.Instance);compatDiagnostics.Free();foreach (var diagnostic in strictDiagnostics.AsEnumerable()){  // If it is a warning (e.g. WRN_AsyncLacksAwaits),      or an error that would be reported by the compatible analysis,      just report it.  if (   diagnostic.Severity != DiagnosticSeverity.Error       || compatDiagnosticSet.Contains(diagnostic))  {    diagnostics.Add(diagnostic);    continue;  }  // Otherwise downgrade the error to a warning.  ErrorCode oldCode = (ErrorCode)diagnostic.Code;  ErrorCode newCode = oldCode switch  {#pragma warning disable format    ErrorCode.ERR_UnassignedThisAutoProperty       => ErrorCode.WRN_UnassignedThisAutoProperty,    ErrorCode.ERR_UnassignedThis                   => ErrorCode.WRN_UnassignedThis,    ErrorCode.ERR_ParamUnassigned                   // <=            => ErrorCode.WRN_ParamUnassigned,    ErrorCode.ERR_UseDefViolationProperty          => ErrorCode.WRN_UseDefViolationProperty,    ErrorCode.ERR_UseDefViolationField             => ErrorCode.WRN_UseDefViolationField,    ErrorCode.ERR_UseDefViolationThis              => ErrorCode.WRN_UseDefViolationThis,    ErrorCode.ERR_UseDefViolationOut               => ErrorCode.WRN_UseDefViolationOut,    ErrorCode.ERR_UseDefViolation                  => ErrorCode.WRN_UseDefViolation,    _ => oldCode, // rare but possible, e.g.                      ErrorCode.ERR_InsufficientStack occurring in                      strict mode only due to needing extra frames#pragma warning restore format  };  ....  var args      = diagnostic is DiagnosticWithInfo {          Info: { Arguments: var arguments }        }        ? arguments        : diagnostic.Arguments.ToArray();  diagnostics.Add(newCode, diagnostic.Location, args);}

Что здесь происходит в нашем случае при использовании CancellationToken? В цикле происходит обход strictDiagnostics (напоминаю, что там содержится ошибка про неинициализированный out-параметр). Then-ветвь оператора if не исполняется, так как diagnostic.Severity имеет значение DiagnosticSeverity.Error, а коллекция compatDiagnosticSet пуста. А далее происходит маппинг кода ошибки компиляции на новый код уже предупреждения, после чего это предупреждение формируется и записывается в результирующую коллекцию. Таким вот образом ошибка компиляции превратилась в предупреждение. :)


Кстати, оно имеет достаточно низкий уровень, поэтому при запуске компилятора данного предупреждения может быть не видно, если не установить флаг выдачи предупреждений соответствующего уровня.


Выставляем запуск компилятора, указав дополнительный флаг: csc.exe %pathToFile% -w:5


И видим ожидаемое предупреждение:


0800_OutParamsCs_ru/image6.png


Теперь мы разобрались, куда пропадает ошибка компиляции, она заменяется на низкоприоритетное предупреждение. Однако у нас до сих пор нет ответа на вопрос, в чём же особенность CancellationToken и его отличие от MyStruct? Почему при анализе метода с out-параметром MyStruct compat анализ находит ошибку, а когда тип параметра CancellationToken ошибка не обнаруживается?


Тут я предлагаю заварить чашечку чая или кофе, так как далее нас ждёт более глубокое погружение.


0800_OutParamsCs_ru/image7.png


Надеюсь, вы воспользовались советом и подготовились. Мы продолжаем. :)


Помните метод ReportUnassignedParameter, в котором происходила запись ошибки компиляции? Поднимаемся немного выше и смотрим вызывающий метод:


protected override void LeaveParameter(ParameterSymbol parameter,                                        SyntaxNode syntax,                                        Location location){  if (parameter.RefKind != RefKind.None)  {    var slot = VariableSlot(parameter);    if (slot > 0 && !this.State.IsAssigned(slot))    {      ReportUnassignedOutParameter(parameter, syntax, location);    }    NoteRead(parameter);  }}

Разница при выполнении этих методов из strict и compat анализа в том, что в первом случае переменная slot имеет значение 1, а во втором -1. Следовательно, во втором случае не выполняется then-ветвь оператора if. Теперь нужно выяснить, почему во втором случае slot имеет значение -1.


Смотрим метод LocalDataFlowPass.VariableSlot:


protected int VariableSlot(Symbol symbol, int containingSlot = 0){  containingSlot = DescendThroughTupleRestFields(                     ref symbol,                      containingSlot,                                                        forceContainingSlotsToExist: false);  int slot;  return     (_variableSlot.TryGetValue(new VariableIdentifier(symbol,                                                       containingSlot),                                out slot))     ? slot     : -1;}

В нашем случае _variableSlot не содержит слота под out-параметр, соответственно, _variableSlot.TryGetValue(....) возвращает значение false, исполнение кода идёт по alternative-ветви оператора ?:, и из метода возвращается значение -1. Теперь нужно понять, почему _variableSlot не содержит out-параметра.


0800_OutParamsCs_ru/image8.png


Покопавшись, находим метод LocalDataFlowPass.GetOrCreateSlot. Выглядит он следующим образом:


protected virtual int GetOrCreateSlot(  Symbol symbol,   int containingSlot = 0,   bool forceSlotEvenIfEmpty = false,   bool createIfMissing = true){  Debug.Assert(containingSlot >= 0);  Debug.Assert(symbol != null);  if (symbol.Kind == SymbolKind.RangeVariable) return -1;  containingSlot     = DescendThroughTupleRestFields(        ref symbol,         containingSlot,        forceContainingSlotsToExist: true);  if (containingSlot < 0)  {    // Error case. Diagnostics should already have been produced.    return -1;  }  VariableIdentifier identifier     = new VariableIdentifier(symbol, containingSlot);  int slot;  // Since analysis may proceed in multiple passes,      it is possible the slot is already assigned.  if (!_variableSlot.TryGetValue(identifier, out slot))  {    if (!createIfMissing)    {      return -1;    }    var variableType = symbol.GetTypeOrReturnType().Type;    if (!forceSlotEvenIfEmpty && IsEmptyStructType(variableType))    {      return -1;    }    if (   _maxSlotDepth > 0         && GetSlotDepth(containingSlot) >= _maxSlotDepth)    {      return -1;    }    slot = nextVariableSlot++;    _variableSlot.Add(identifier, slot);    if (slot >= variableBySlot.Length)    {      Array.Resize(ref this.variableBySlot, slot * 2);    }    variableBySlot[slot] = identifier;  }  if (IsConditionalState)  {    Normalize(ref this.StateWhenTrue);    Normalize(ref this.StateWhenFalse);  }  else  {    Normalize(ref this.State);  }  return slot;}

Из метода видно, что есть ряд условий, когда метод вернёт значение -1, а слот не будет добавлен в _variableSlot. Если же слота под переменную ещё нет, и все проверки проходят успешно, то происходит запись в _variableSlot: _variableSlot.Add(identifier, slot). Отлаживаем код и видим, что при выполнении strict анализа все проверки успешно проходят, а вот при compat анализе мы заканчиваем выполнение метода в следующем операторе if:


var variableType = symbol.GetTypeOrReturnType().Type;if (!forceSlotEvenIfEmpty && IsEmptyStructType(variableType)){  return -1;}

Значение переменной forceSlotEvenIfEmpty в обоих случаях одинаковое (false), а разница в том, какое значение возвращает метод IsEmptyStructType: для strict анализа false, для compat анализа true.


0800_OutParamsCs_ru/image9.png


Здесь сразу же возникают новые вопросы и желание поэкспериментировать. То есть получается, что, если тип out-параметра "пустая структура" (позже мы поймём, что это значит), компилятор считает такой код допустимым и не генерирует ошибку? Убираем в нашем примере из MyStruct поле и компилируем.


struct MyStruct{  }void CheckYourself(out MyStruct obj){  // Do nothing}

И этот код успешно компилируется! Интересно Упоминаний таких особенностей в документации и спецификации я что-то не помню. :)


Но тогда возникает другой вопрос: а как же работает код в случае, когда тип out-параметра CancellationToken? Ведь это явно не "пустая структура" если посмотреть код на referencesource.microsoft.com (ссылка на CancellationToken), становится видно, что этот тип содержит и методы, и свойства, и поля Непонятно, копаем дальше.


Мы остановились на методе LocalDataFlowPass.IsEmptyStructType:


protected virtual bool IsEmptyStructType(TypeSymbol type){  return _emptyStructTypeCache.IsEmptyStructType(type);}

Идём глубже (EmptyStructTypeCache.IsEmptyStructType):


public virtual bool IsEmptyStructType(TypeSymbol type){  return IsEmptyStructType(type, ConsList<NamedTypeSymbol>.Empty);}

И ещё глубже:


private bool IsEmptyStructType(  TypeSymbol type,   ConsList<NamedTypeSymbol> typesWithMembersOfThisType){  var nts = type as NamedTypeSymbol;  if ((object)nts == null || !IsTrackableStructType(nts))  {    return false;  }  // Consult the cache.  bool result;  if (Cache.TryGetValue(nts, out result))  {    return result;  }  result = CheckStruct(typesWithMembersOfThisType, nts);  Debug.Assert(!Cache.ContainsKey(nts) || Cache[nts] == result);  Cache[nts] = result;  return result;}

Выполнение кода идёт через вызов метода EmptyStructTypeCache.CheckStruct:


private bool CheckStruct(  ConsList<NamedTypeSymbol> typesWithMembersOfThisType,   NamedTypeSymbol nts){  ....   if (!typesWithMembersOfThisType.ContainsReference(nts))  {    ....    typesWithMembersOfThisType       = new ConsList<NamedTypeSymbol>(nts,                                       typesWithMembersOfThisType);    return CheckStructInstanceFields(typesWithMembersOfThisType, nts);  }  return true;}

Здесь исполнение заходит в then-ветвь оператора if, т.к. коллекция typesWithMembersOfThisType пустая (см. метод EmptyStructTypeCache.IsEmptyStructType, где она начинает передаваться в качестве аргумента).


Какая-то картина уже начинает вырисовываться теперь становится понятно, что такое "пустая структура". Судя по названиям методов, это такая структура, которая не содержит экземплярных полей. Но я напоминаю, что в CancellationToken экземплярные поля есть. Значит, идём ещё глубже, в метод EmptyStructTypeCache.CheckStructInstanceFields.


private bool CheckStructInstanceFields(  ConsList<NamedTypeSymbol> typesWithMembersOfThisType,   NamedTypeSymbol type){  ....  foreach (var member in type.OriginalDefinition                             .GetMembersUnordered())  {    if (member.IsStatic)    {      continue;    }    var field = GetActualField(member, type);    if ((object)field != null)    {      var actualFieldType = field.Type;      if (!IsEmptyStructType(actualFieldType,                              typesWithMembersOfThisType))      {        return false;      }    }  }  return true;}

В методе обходятся экземплярные члены, для каждого из которых получается 'actualField'. Дальше, если удалось получить это значение (field не null) опять выполняется проверка: а является ли тип этого поля "пустой структурой"? Соответственно, если нашли хотя бы одну "не пустую структуру", изначальный тип также считаем "не пустой структурой". Если все экземплярные поля "пустые структуры", то изначальный тип также считается "пустой структурой".


Придётся опуститься ещё немного глубже. Не беспокойтесь, скоро наше погружение закончится, и мы расставим точки над 'i'. :)


Смотрим метод EmptyStructTypeCache.GetActualField:


private FieldSymbol GetActualField(Symbol member, NamedTypeSymbol type){  switch (member.Kind)  {    case SymbolKind.Field:      var field = (FieldSymbol)member;      ....      if (field.IsVirtualTupleField)      {        return null;      }      return (field.IsFixedSizeBuffer ||               ShouldIgnoreStructField(field, field.Type))             ? null             : field.AsMember(type);      case SymbolKind.Event:        var eventSymbol = (EventSymbol)member;        return (!eventSymbol.HasAssociatedField ||                ShouldIgnoreStructField(eventSymbol, eventSymbol.Type))              ? null              : eventSymbol.AssociatedField.AsMember(type);  }  return null;}

Соответственно, для типа CancellationToken нас интересует case-ветвь SymbolKind.Field. В неё мы можем попасть только при анализе члена m_source этого типа (т.к. тип CancellationToken содержит только одно экземплярное поле m_source).


Рассмотрим, как происходят вычисления в этой case-ветви в нашем случае.


field.IsVirtualTupleField false. Переходим к условному оператору и разберём условное выражение field.IsFixedSizeBuffer || ShouldIgnoreStructField(field, field.Type). field.IsFixedSizeBuffer не наш случай. Значение, ожидаемо, false. А вот значение, возвращаемое вызовом метода ShouldIgnoreStructField(field, field.Type), различается для strict и compat анализа (напоминаю, мы анализируем одно и то же поле одного и того же типа).


Смотрим тело метода EmptyStructTypeCache.ShouldIgnoreStructField:


private bool ShouldIgnoreStructField(Symbol member,                                      TypeSymbol memberType){  // when we're trying to be compatible with the native compiler, we      ignore imported fields (an added module is imported)     of reference type (but not type parameters,      looking through arrays)     that are inaccessible to our assembly.  return _dev12CompilerCompatibility &&                                      ((object)member.ContainingAssembly != _sourceAssembly ||             member.ContainingModule.Ordinal != 0) &&                               IsIgnorableType(memberType) &&                                          !IsAccessibleInAssembly(member, _sourceAssembly);          }

Посмотрим, что отличается для strict и compat анализа. Хотя, возможно, вы уже догадались самостоятельно. :)


Strict анализ: _dev12CompilerCompatibility false, следовательно, результат всего выражения false. Compat анализ: значения всех подвыражений true, результат всего выражения true.


А теперь сворачиваем цепочку, поднимаясь с самого конца. :)


При compat анализе мы считаем, что должны игнорировать единственное экземплярное поле типа CancellationSource m_source. Таким образом, мы считаем, что CancellationToken "пустая структура", следовательно для неё не создаётся слот, и не происходит записи в кэш "пустых структур". Так как слот отсутствует, мы не обрабатываем out-параметр и не записываем ошибку компиляции при выполнении compat анализа. Как результат, strict и compat анализ дают разные результаты, из-за чего происходит понижение ошибки компиляции до низкоприоритетного предупреждения.


То есть это не какая-то особая обработка типа CancellationToken есть целый ряд типов, для которых отсутствие инициализации out-параметра не будет приводить к ошибкам компиляции.


Давайте попробуем посмотреть на практике, для каких типов компиляция будет успешно проходить. Как обычно, берём наш типичный метод:


void CheckYourself(out MyType obj){  // Do nothing}

И пробуем подставлять вместо MyType различные типы. Мы уже разобрали, что этот код успешно компилируется для CancellationToken и для пустой структуры. Что ещё?


struct MyStruct{ }struct MyStruct2{  private MyStruct _field;}

Если вместо MyType используем MyStruct2, код также успешно компилируется.


public struct MyExternalStruct{  private String _field;}

При использовании этого типа код будет успешно компилироваться, если MyExternalStruct объявлен во внешней сборке. Если в одной сборке с методом CheckYourself не скомпилируется.


При использовании такого типа из внешней сборки код уже не скомпилируется (поменяли уровень доступа поля _field с private на public):


public struct MyExternalStruct{  public String _field;}

При таком изменении типа код тоже не будет компилироваться (поменяли тип поля со String на int):


public struct MyExternalStruct{  private int _field;}

В общем, как вы поняли, здесь есть определённый простор для экспериментов.


Подытожим


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


Что же по поводу типов, для которых можно не инициализировать out-параметры? Например, не обязательна инициализация параметра, если тип структура, в которой нет полей. Или если все поля структуры без полей. Или вот случай с CancellationToken: с ним компиляция успешно проходит, так как этот тип находится во внешней библиотеке, единственное поле m_source ссылочного типа, а само поле недоступно из внешнего кода. В принципе, несложно придумать и составить ещё своих подобных типов, при использовании которых вы сможете не инициализировать out-параметры и успешно компилировать ваш код.


Возвращаясь к вопросу из начала статьи:


void CheckYourself(out MyStruct obj){  // Do nothing}public struct MyStruct{ .... }

Компилируется ли этот код? Как вы уже поняли, ни 'Да', ни 'Нет' не являются правильным ответом. В зависимости от того, что такое MyStruct (какие есть поля, где объявлен тип и т. п.), этот код может либо компилироваться, либо не компилироваться.


Заключение


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


Кстати, приглашаю подписаться на мой аккаунт в Twitter, где я также выкладываю статьи и прочие интересные находки. Так точно ничего интересного не пропустите. :)


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. Should We Initialize an Out Parameter Before a Method Returns?.

Подробнее..

Оптимизация .NET приложения как простые правки позволили ускорить PVS-Studio и уменьшить потребление памяти на 70

15.06.2021 18:13:51 | Автор: admin

Проблемы с производительностью, такие как аномально низкая скорость работы и высокое потребление памяти, могут быть обнаружены самыми разными способами. Такие недостатки приложения выявляются тестами, самими разработчиками или тестировщиками, а при менее удачном раскладе пользователями. Увы, но обнаружение аномалий лишь первый шаг. Далее проблему необходимо локализовать, ведь в противном случае решить её не получится. Тут возникает вопрос как найти в большом проекте причины, приводящие к излишнему потреблению памяти и замедлению работы? Есть ли они вообще? Быть может, дело и не в приложении вовсе? Эта статья посвящена истории о том, как разработчики C#-анализатора PVS-Studio столкнулись с подобной проблемой и смогли решить её.

Бесконечный анализ

Анализ крупных C#-проектов всегда занимает некоторое время. Это ожидаемо PVS-Studio погружается в исследование исходников достаточно глубоко и использует при этом различные технологии, такие как межпроцедурный анализ, анализ потока данных и т.д. Тем не менее анализ многих крупных проектов, найденных нами на github, производится не дольше нескольких часов.

Возьмём, к примеру, Roslyn. Его solution включает более 200 проектных файлов, и почти все из них проекты на C#. Нетрудно догадаться, что в каждом из проектов далеко не по одному файлу, а сами файлы состоят далеко не из пары строчек кода. PVS-Studio проводит полный анализ Roslyn примерно за 1,5-2 часа. Конечно, некоторые проекты наших пользователей требуют гораздо больше времени на анализ, но ситуации, когда анализ не проходит даже за сутки, исключительны.

Именно в такой ситуации и оказался один из наших клиентов. Он написал в поддержку, что анализ его проекта не проходит... Даже за 3 дня! Тут явно было что-то не так. Очевидно, мы не могли оставить подобную проблему без внимания.

Стоп, а как же тестирование?!

Наверняка у читателя возникает логичный вопрос почему же проблема не была выявлена на этапе тестирования? Как же так вышло, что она была обнаружена именно клиентом? Неужели C#-анализатор PVS-Studio не тестируется?

Тестируется и тщательно! Для нас тестирование является неотъемлемой частью процесса разработки. Корректность работы анализатора постоянно проверяется, ровно как проверяется и корректность работы отдельных его частей. Без преувеличения можно сказать, что unit-тесты диагностических правил и внутренних механизмов составляют примерно половину от общего объёма исходного кода проекта C#-анализатора. Кроме того, каждую ночь на сервере производится анализ большого набора проектов и проверка корректности формируемых анализатором отчётов. При этом автоматически определяется как скорость работы анализатора, так и объём потребляемой памяти. Более-менее существенные отклонения от нормы мгновенно обнаруживаются и исследуются.

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

Поиск причин

Дамп

Мы предположили, что проблема могла быть связана с какими-то особенностями проекта нашего клиента. Мы знали, что этот проект достаточно крупный и сложный, но этой информации было недостаточно. Требовались детали.

Эти детали нам мог дать дамп памяти процесса анализатора. Что это такое? Если вкратце, то дамп это срез данных из оперативной памяти. С его помощью мы решили выяснить, какие данные загружены в рабочую память процесса PVS-Studio. В первую очередь нас интересовали какие-нибудь аномалии, которые могли стать причиной столь сильного замедления работы.

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

От файла с дампом мало толку, если нет возможности его открыть. К счастью, пользователю этим заниматься уже не нужно :). Ну а мы решили изучить данные дампа при помощи Visual Studio. Делается это достаточно просто:

  1. Открываем проект с исходниками приложения в Visual Studio.

  2. В верхнем меню нажимаем File->Open->File (или Ctrl+O).

  3. Находим файл с дампом и открываем.

В результате появится окошко с кучей различной информации о процессе:

Нас в первую очередь интересовала возможность перехода в своеобразный режим отладки дампа. Для этого нужно нажать кнопку Debug With Managed Only.

Примечание. Если вас интересует более подробная информация по теме открытия дампов через Visual Studio для отладки, то отличным источником информации будет официальная документация.

Итак, мы перешли в режим отладки. Отладка дампа достаточно мощный механизм, но важно помнить и о некоторых ограничениях:

  • отсутствует какая-либо возможность возобновления работы процесса, пошагового выполнения кода и т.п.;

  • в окне Quick Watch и Immediate Window невозможно использовать некоторые функции. К примеру, попытка вызова метода File.WriteAllText приводила к возникновению ошибки "Caracteres no vlidos en la ruta de acceso!". Дело в том, что дамп связан с окружением, на котором он был снят.

Отладка дампа позволила нам получить достаточно большое количество различных данных. Ниже представлена небольшая часть информации о состоянии процесса анализа в момент снятия дампа:

  • вычисленное количество файлов в проекте: 1 500;

  • приблизительное время анализа: 24 часа;

  • количество одновременно анализируемых в текущий момент файлов: 12;

  • количество уже проверенных файлов: 1060.

Работа с дампом позволила нам сделать некоторые выводы. Было ясно, что анализ большей части файлов проекта к моменту снятия дампа уже прошёл. Из этого следовало, что замедление стало проявляться ближе к концу анализа. У нас появилось предположение, что факторы, приводящие к замедлению, могли накапливаться.

Тем не менее, понять причины замедления нам не удалось. Мы не обнаружили каких-либо аномалий или ошибок, а количество файлов в проекте не казалось чем-то из ряда вон выходящим. Анализ проекта похожего объёма обычно занимает у нас около 2 часов.

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

Наконец-то, воспроизведение проблемы

Используя данные из дампа, мы поняли, что анализ "завис" на конкретных файлах со сложной структурой кода. Мы попросили их у клиента, надеясь воспроизвести возникшую проблему. Однако при анализе отдельных файлов проблема не воспроизвелась.

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

Мы создали свой тестовый проект, стараясь повторить следующие характеристики проекта пользователя:

  • количество файлов;

  • средний размер файлов;

  • максимальный уровень вложенности и сложность используемых конструкций.

Скрестив пальцы, мы запустили его анализ и...

Никаких замедлений. После стольких приложенных усилий мы так и не смогли воспроизвести проблему. Анализ сформированного проекта проходил за вполне адекватное время и успешно завершался. Ни зависаний, ни ошибок, ни каких-то аномалий. В такие моменты можно всерьёз задуматься а не решил ли пользователь над нами пошутить?

Казалось, что мы уже всё перепробовали. Казалось, что докопаться до правды не выйдет. А ведь мы бы и рады были заняться исследованием проблемы с замедлением! Мы бы и рады были одолеть её, наконец, порадовать клиента, порадоваться самим. Как ни крути, анализ проекта нашего пользователя не должен был зависать!

Поддержка клиентов сложная работа, порой требующая невероятного упорства. Мы продолжили копать. Снова и снова мы пытались воспроизвести проблему и вдруг... У нас получилось.

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

А отличие было в железе. Точнее говоря, в ОЗУ.

Казалось бы, при чём тут ОЗУ?

Наши автоматизированные тесты проводятся на сервере с 32 Гб доступной оперативной памяти. На компьютерах сотрудников её объём различается, но везде есть по крайней мере 16 гигабайт, а у большинства 32 и более. Воспроизвести же баг удалось на ноутбуке, объём оперативной памяти которого составлял 8 Гб.

Возникает логичный вопрос к чему это всё? Мы же решали проблему замедления работы, а не высокого потребления памяти!

Дело в том, что высокое потребление памяти действительно может приводить к замедлению работы приложения. Это происходит в тех случаях, когда процессу не хватает памяти, установленной на устройстве. В таких случаях активируется особый механизм memory paging (другое название "swapping"). При его работе часть данных из оперативной памяти переносится во вторичное хранилище (диск). При необходимости система загружает данные с диска. Благодаря данному механизму приложения могут использовать оперативную память в большем объёме, чем доступно в системе. Увы, но у этого чуда есть своя цена.

Состоит эта цена в ощутимом сокращении скорости работы. Операции с жёстким диском производятся намного медленнее, чем работа с оперативной памятью. И именно swapping сильнее всего замедлял работу нашего анализатора.

В принципе, на этом можно было и закончить. Сообщили бы пользователю, что всё у него будет хорошо, если увеличить объём доступной оперативной памяти, и ладно. Однако это вряд ли бы удовлетворило клиента, да и самим нам такой вариант совсем не нравился. Поэтому мы решили изучить вопрос потребления памяти более детально.

Решаем проблему

dotMemory и диаграмма доминаторов

Мы использовали приложение dotMemory, разработанное компанией JetBrains. Это профилировщик памяти для .NET, который можно запускать как прямо из Visual Studio, так и в качестве отдельного инструмента. Среди всех возможностей dotMemory более всего нас интересовало профилирование процесса анализа.

Ниже представлено окно присоединения к процессу:

Сначала нужно запустить соответствующий процесс, затем выбрать его и начать профилирование с помощью кнопки "Run". Откроется новое окно:

В любой момент времени можно получить снимок состояния памяти. За время работы процесса можно сделать несколько таких снимков все они появятся на панели "Memory Snapshots":

Далее нам необходимо детально изучить полученный снимок (для этого нужно кликнуть по его идентификатору). Открывшееся окно будет содержать большое количество различных элементов:

Более полную информацию о работе с dotMemory, включая подробное описание представленных здесь данных, можно найти в официальной документации. Нам же была особенно интересна sunburst диаграмма, показывающая иерархию доминаторов объектов, эксклюзивно удерживающих другие объекты в памяти. Для перехода к ней необходимо открыть вкладку "Dominators".

Все указанные действия мы проделали с процессом анализа специально созданного тестового проекта. Диаграмма доминаторов для него выглядела следующим образом:

Объекты высокого уровня были неособенно интересны, ведь сами по себе они не занимали много места. Куда важнее было узнать, что именно содержится "внутри". Какие же объекты размножились настолько, что начали занимать так много места?

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

Анализ потока данных (Data-Flow Analysis) заключается в вычислении возможных значений переменных в различных точках компьютерной программы. Например, если ссылка разыменовывается и при этом известно, что в текущий момент она может быть равна null, то это потенциальная ошибка, и статический анализатор сообщит о ней. Подробнее об этой и других технологиях, использующихся в PVS-Studio, можно прочитать в статье.

Вычисленные диапазоны значений переменных кешируются в целях оптимизации работы, что, к сожалению, приводит к серьёзному увеличению объёма потребляемой оперативной памяти. При этом убирать механизм кеширования нельзя! Ведь, к примеру, работа межпроцедурного анализа без использования этого механизма замедлилась бы на порядок.

Что же тогда делать? Неужели опять тупик?

А не такие уж они и разные

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

Мы решили повнимательнее взглянуть на значения в кеше. Оказалось, что PVS-Studio хранил большое количество абсолютно идентичных объектов. К примеру, для многих переменных анализатор не может вычислить значение, так как оно может быть любым (в пределах ограничений своего типа):

void MyFunction(int a, int b, int c ....){  // a = ?  // b = ?  // c = ?  ....}

При этом каждой переменной соответствовал свой собственный объект значения. Получается, что таких объектов была целая куча, но они ничем друг от друга не отличались!

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

А вот и нет! На самом деле, нужно совсем немного:

  • некоторое хранилище, в котором будут находиться уникальные значения переменных;

  • механизмы доступа к хранилищу добавление новых и получение существующих элементов;

  • переработка некоторых фрагментов, связанных с добавлением новых виртуальных значений в кеш.

Изменения в отдельных частях анализатора затрагивали, как правило, пару строк. Реализация хранилища также не заняла много времени. В результате кеш стал хранить только уникальные значения.

Вполне возможно, вам знаком описанный подход. Сделанное нами пример реализации известного паттерна Flyweight. Цель его применения оптимизация работы с памятью путём предотвращения создания экземпляров элементов, имеющих общую сущность.

Кроме того, можно вспомнить и такое понятие, как интернирование строк. По сути то же самое: если строки одинаковы по значению, то фактически они будут представлены одним и тем же объектом. В C# строковые литералы интернируются автоматически. Для прочих строк можно использовать методы string.Intern и string.IsInterned. Однако не всё так просто. Даже этим механизмом нужно пользоваться с умом. Если вам интересна данная тема, то предлагаю к прочтению статью "Подводные камни в бассейне строк, или ещё один повод подумать перед интернированием экземпляров класса String в C#".

Выигранная память

Мы внесли несколько мелких правок, реализовав паттерн Flyweight. Каковы были результаты?

Они были невероятны! Пиковое потребление оперативной памяти при проверке тестового проекта уменьшилось с 14,55 до 4,73 гигабайт. Столь простое и быстрое решение позволило уменьшить расход памяти примерно на 68%! Мы были шокированы и очень довольны результатом. Доволен был и клиент теперь ОЗУ его компьютера хватало, а значит, и анализ начал проходить за адекватное время.

Достигнутый результат действительно радовал, но...

Нужно больше оптимизаций!

Да, мы смогли уменьшить потребление памяти. Однако изначально мы же хотели ускорить анализ! Конечно, он действительно ускорился у клиента, как и на других машинах, где не хватало ОЗУ. Но ускорения на мощных компьютерах мы не добились только сократили потребление памяти. А раз уж мы столь глубоко погрузились в эту тему... Почему бы не продолжить?

dotTrace

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

Ответы на наши вопросы мог дать dotTrace хороший профилировщик производительности для .NET приложений, предоставляющий ряд интересных возможностей. Интерфейс этого приложения довольно сильно напоминает dotMemory:

Примечание. Как и в случае с dotMemory, в этой статье не будет подробного руководства по использованию dotTrace и описания всех особенностей работы с данным приложением. Любые интересующие детали вы можете уточнить в документации. Здесь же я лишь в общем расскажу, какие действия мы предпринимали, чтобы найти возможности для оптимизации скорости работы.

Итак, используя dotTrace, мы запустили анализ одного большого проекта. Ниже показан пример окна, отображающего в реальном времени графики использования процессом памяти и CPU:

Чтобы начать "запись" данных о работе приложения, нужно нажать Start (по умолчанию процесс сбора данных начинается сразу). Подождав некоторое время, нажимаем "Get Snapshot And Wait". Перед нами отображается окно с собранными данными. Например, для простого консольного приложения это окно выглядит так:

Здесь нам доступно большое количество различной информации. В первую очередь интересно время работы отдельных методов. Также может быть полезно узнать время работы потоков. Доступна и возможность рассмотрения общего отчёта для этого нужно кликнуть в верхнем меню View->Snapshot Overview или использовать комбинацию Ctrl+Shift+O.

Уставший сборщик мусора

Что же мы смогли выяснить благодаря dotTrace? Ну, во-первых, мы в очередной раз убедились, что C#-анализатор не использует процессорные мощности даже наполовину. PVS-Studio C# многопоточное приложение, и, по идее, нагрузка на процессор должна быть ощутимой. Несмотря на это, при анализе загрузка процессора часто падала до 1315% общей мощности CPU. Очевидно, работаем неэффективно, но почему?

dotTrace показал нам, что большую часть времени анализа работает даже не само приложение, а сборщик мусора! Возникает логичный вопрос как же так?

Дело в том, что запуск сборки блокировал потоки анализатора. Сборка завершалась, анализатор немного поработал и снова запускается сборка мусора, а PVS-Studio "отдыхает".

Осознав проблему, мы поняли, что нужно отыскать места, в которых выделение памяти под новые объекты производится наиболее активно. Затем нужно было проанализировать найденные фрагменты и внести необходимые для оптимизации изменения.

Мы не виноваты, это всё их DisplayPart!

Трассировщик показал, что наиболее часто память выделяется под объекты типа DisplayPart. При этом они существуют достаточно недолго, а значит, и освобождать память от них приходится часто.

Возможно, мы могли бы вообще отказаться от использования этих объектов, если бы не один нюанс. В исходниках нашего C#-анализатора DisplayPart даже не упоминается! Как оказалось, этот тип играет определённую роль в используемом нами Roslyn API.

Roslyn (или .NET Compiler Platform) является основой C#-анализатора PVS-Studio. Он предоставляет нам готовые решения для ряда задач:

  • преобразование файла с исходным кодом в синтаксическое дерево;

  • удобный способ обхода синтаксического дерева;

  • получение различной (в том числе семантической) информации о конкретном узле дерева;

  • и т.д.

Roslyn платформа с открытым исходным кодом. Это позволило без проблем понять, что такое *DisplayPart *и зачем этот тип вообще нужен.

Оказалось, что объекты DisplayPart активно используются при создании строковых представлений так называемых символов. Если не погружаться в детали, то символ это объект, содержащий семантическую информацию о некоторой сущности в исходном коде. К примеру, символ метода позволяет получить данные о параметрах данного метода, классе-родителе, возвращаемом типе и т.д. Более подробно данная тема освещена в статье "Введение в Roslyn. Использование для разработки инструментов статического анализа". Очень рекомендую к прочтению всем, кто интересуется статическим анализом (вне зависимости от предпочитаемого языка программирования).

Строковые представления некоторых символов нам действительно приходилось получать, и делали мы это с помощью вызова метода ToString. Как оказалось, внутри отрабатывал сложный алгоритм, активно создающий объекты типа DisplayPart. Проблема состояла в том, что алгоритм отрабатывал каждый раз, когда нам было необходимо получить строковое представление (то есть довольно часто).

Как это обычно и бывает, локализация проблемы = 90% её решения. Раз уж вызовы ToString у символов создают столько проблем, то, может, и не стоит производить их?

Увы, полностью отказаться от получения строкового представления было нельзя. Поэтому мы решили по крайней мере с нашей стороны минимизировать количество вызовов ToString у символов.

Решение было простым мы стали кешировать получаемые строковые представления. Таким образом, для каждого символа алгоритм получения строкового представления отрабатывал не более одного раза. Ну, во всяком случае, в отдельно взятом потоке. Просто мы решили, что наиболее оптимальным вариантом будет использование собственного кеша для каждого потока. Это позволяло обойтись без синхронизации между потоками, а дублирование некоторых значений было незначительным.

Описанная правка, вопреки ожиданиям, практически не увеличила загрузку процессора (изменение составляло буквально несколько процентов). Тем не менее, PVS-Studio стал работать значительно быстрее: один из наших тестовых проектов ранее анализировался 2,5 часа, а после правок анализ проходил всего за 2. Ускорение работы на 20% действительно радовало.

Упакованный Enumerator

На втором месте по количеству выделяемой памяти были объекты типа List<T>.Enumerator, используемые при обходе соответствующих коллекций. Итератор списка является структурой, а значит, создаётся на стеке. Тем не менее, трассировка показывала, что такие объекты в больших количествах попадали в кучу! С этим нужно было разобраться.

Объект значимого типа может попасть в кучу в результате упаковки (boxing). Она выполняется при приведении объекта значимого типа к object или реализуемому интерфейсу. Итератор списка реализует интерфейс IEnumerator, и именно приведение к этому интерфейсу вело к попаданию итератора в кучу.

Для получения объекта Enumerator используется метод GetEnumerator. Общеизвестно, что это метод, определённый в интерфейсе IEnumerable. Взглянув на его сигнатуру, можно заметить, что возвращаемый тип данного метода IEnumerator. Получается, что вызов GetEnumerator у списка всегда приводит к упаковке?

А вот и нет! Метод GetEnumerator, определённый в классе List, возвращает структуру:

Так всё-таки будет упаковка производиться или нет? Ответ на этот вопрос зависит от типа ссылки, у которой вызывается GetEnumerator:

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

Конечно, разница невелика, если такой *Enumerator *создаётся лишь пару сотен раз за время работы программы. Однако при анализе более-менее объёмного проекта в нашем C#-анализаторе эти объекты создаются миллионы или даже десятки миллионов раз. В таких случаях разница становится весьма ощутимой.

Примечание. Как правило, мы не вызываем GetEnumerator напрямую. Зато достаточно часто приходится использовать цикл foreach. Именно он "под капотом" получает итератор. Если в foreach передана ссылка типа List, то и итератор, используемый в foreach, будет лежать на стеке. Если же с помощью foreach производится обход абстрактного IEnumerable, то итератор будет сохранён в куче, а foreach будет работать со ссылкой типа IEnumerator. Описанное поведение актуально и для других коллекций, в которых присутствует GetEnumerator, возвращающий итератор значимого типа.

Конечно, нельзя полностью отказаться от использования IEnumerable. Однако в коде анализатора мы обнаружили множество мест, где метод принимал в качестве аргумента абстрактный IEnumerable, а при его вызове всегда передавался вполне конкретный List.

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

И ты, LINQ?!

Методы расширения, определённые в пространстве имён System.Linq, используются для работы с коллекциями повсеместно. Достаточно часто они действительно позволяют упростить код. Наверное, ни один более-менее серьёзный проект не обходится без использования всеми любимых методов Where, Select и т. д. C#-анализатор PVS-Studio не исключение.

Что ж, красота и удобство LINQ-методов дорого нам обошлись. Так дорого, что во многих местах мы отказались от их использования в пользу простого foreach. Как же так вышло?

Основная проблема снова состояла в создании огромного количества объектов, реализующих интерфейс IEnumerator. Такие объекты создаются на каждый вызов LINQ-метода. Взгляните на следующий код:

List<int> sourceList = ....var enumeration = sourceList.Where(item => item > 0)                            .Select(item => someArray[item])                            .Where(item => item > 0)                            .Take(5);

Сколько итераторов будет создано при его выполнении? Давайте посчитаем! Чтобы понять, как всё это работает, откроем исходники System.Linq. Они доступны на github по ссылке.

При вызове Where будет создан объект класса WhereListIterator особая версия Where-итератора, оптимизированная для работы с List (похожая оптимизация есть и для массивов). Данный итератор хранит внутри ссылку на список. При переборе коллекции WhereListIterator сохранит в себе итератор списка, после чего будет использовать его при работе. Так как WhereListIterator рассчитан именно на список, то приведение итератора к типу IEnumerator не производится. Однако сам *WhereListIterator *является классом, а значит, его экземпляры попадут в кучу. Следовательно, исходный итератор в любом случае будет храниться не на стеке.

Вызов Select приведёт к созданию объекта класса WhereSelectListIterator. Очевидно, и он будет храниться в куче.

Последующие вызовы Where и *Take *также приведут к созданию итераторов и выделению памяти под них.

Итого в куче будет выделена память под 5 итераторов. Позже её придётся освобождать, что станет дополнительной работой для сборщика мусора.

Теперь взглянем на фрагмент, написанный с использованием foreach:

List<int> sourceList = ....List<int> result = new List<int>();foreach (var item in sourceList){  if (item > 0)  {    var arrayItem = someArray[item];    if (arrayItem > 0)    {      result.Add(arrayItem);      if (result.Count == 5)        break;    }  }}

Давайте попробуем проанализировать сравнить подходы с foreach и LINQ.

  • Преимущества варианта с LINQ-вызовами:

    • короче, приятнее выглядит и в целом лучше читается;

    • не требует создания коллекции для хранения результата;

    • вычисление значений будет произведено только при обращении к элементам;

    • в большинстве случаев объект, полученный в результате запроса, хранит только один элемент последовательности.

  • Недостатки варианта с LINQ-вызовами:

    • память в куче выделяется гораздо чаще: в первом примере туда попадает 5 объектов, а во втором только 1 (список result);

    • повторные обходы последовательности приводят к повторному выполнению обхода с выполнением всех указанных функций. Случаи, когда такое поведение действительно полезно, довольно редки. Конечно, можно использовать методы типа ToList, однако это сводит преимущества варианта с LINQ-вызовами на нет (кроме первого).

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

При всём этом мы заметили, что в подавляющем большинстве случаев у нас совершенно не было интереса в отложенном выполнении. Либо для результата LINQ-операций сразу вызывался какой-нибудь ToList, либо код запросов выполнялся по несколько раз при повторных обходах коллекции (что не есть хорошо).

*Замечание. *На самом деле существует простой способ реализовать отложенное выполнение и не плодить при этом лишние итераторы. Возможно, вы догадались, что я говорю о ключевом слове yield. С его помощью можно реализовывать генерацию последовательности элементов, задавать любые правила и условия добавления элементов в последовательность. Подробнее о возможностях yield в C# (а также о том, как эта штука работает внутри) можно найти в статье "Что такое yield и как он работает в C#?".

Изучив внимательно код анализатора, мы обнаружили множество мест, в которых оптимальнее использовать foreach вместо LINQ-методов. Это позволило существенно сократить количество необходимых операций выделения памяти в куче и сборки мусора.

Что же в итоге?

Успех!

Оптимизация работы PVS-Studio прошла успешно! Мы добились успехов в уменьшении потребляемой памяти, а также серьёзно увеличили скорость анализа (на некоторых проектах скорость работы увеличилась более чем на 20%, а пиковое потребление памяти сократилось практически на 70%!). А ведь всё начиналось с непонятной истории клиента о том, как он три дня не мог проверить свой проект! Тем не менее, на этом оптимизация работы анализатора не заканчивается, и мы продолжаем находить новые способы совершенствования PVS-Studio.

Изучение проблем заняло у нас куда больше времени, чем их решение. Но рассказанная история произошла очень давно. Сейчас, как правило, подобные вопросы решаются командой PVS-Studio куда быстрее. Главными помощниками в исследовании проблем выступают различные инструменты, такие как трассировщик и профилировщик. В этой статье я рассказывал о нашем опыте работы с dotMemory и dotPeek, однако это вовсе не означает, что эти приложения единственные в своём роде. Пожалуйста, напишите в комментариях, какими инструментами в таких случаях пользуетесь вы.

Это ещё не конец

Да, мы действительно решили проблему клиента и даже ускорили анализатор в целом, однако... Очевидно, он работает далеко не так быстро, как мог бы. PVS-Studio всё ещё недостаточно активно использует процессорные мощности. Проблема состоит не совсем в алгоритмах анализа проверка каждого файла в отдельном потоке позволяет обеспечивать достаточно высокий уровень параллелизма. Основной бедой производительности C#-анализатора является сборщик мусора, который очень часто блокирует работу всех потоков, что сильно замедляет PVS-Studio. Даже если анализатор будет использовать сотни ядер, скорость работы будет снижаться из-за частой блокировки потоков сборщиком, который, в свою очередь, не может использовать все доступные мощности при выполнении своих задач в силу некоторых алгоритмических ограничений.

Однако и это не тупик, а лишь ещё одно препятствие, которое мы должны преодолеть. Некоторое время назад до меня дошла "секретная информация" о планах реализации процесса анализа... в нескольких процессах! Это позволит обойти существующие ограничения, ведь сборка мусора в одном из процессов не будет влиять на анализ, выполняющийся в других. Подобный подход позволит эффективно использовать большое количество ядер в том числе и с использованием Incredibuild. Кстати, похожим образом уже работает C++ анализатор, для которого давным-давно доступна возможность распределённого анализа.

Откуда ещё берутся проблемы с производительностью

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

Современные IDE позволяют обнаружить некоторые проблемные моменты. Тем не менее, они изучают код достаточно поверхностно, из-за чего получается найти только самые очевидные ошибки типа неиспользуемой переменной или параметра. Более сложные ошибки могут быть найдены с помощью специализированных инструментов для статического анализа. Такие инструменты изучают код гораздо глубже, хотя на это и требуется больше времени. Статический анализатор способен найти множество различных ошибок, включая и те, что приводят к проблемам со скоростью работы и потреблением памяти.

PVS-Studio один из таких анализаторов. Он использует серьёзные технологии вроде межпроцедурного анализа или анализа потока данных, что позволяет значительно повысить надёжность кода любого приложения. Кроме того, одним из наиболее приоритетных направлений работы компании является поддержка пользователей, решение их вопросов и возникающих проблем, а в некоторых случаях мы даже добавляем по просьбе клиента новый функционал :). Смело пишите нам по всем возникающим вопросам! А чтобы попробовать анализатор в деле вы можете перейти по ссылке. Удачного использования!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Lipilin. .NET Application Optimization: Simple Edits Speeded Up PVS-Studio and Reduced Memory Consumption by 70%.

Подробнее..

Как WCF сам себе в ногу стреляет посредством TraceSource

21.06.2021 14:12:41 | Автор: admin

Не так часто удается написать что-то интересное про проблемы, связанные с параллельным программированием. В этот же раз "повезло". Из-за особенностей реализации стандартного метода TraceEvent произошла ошибка с блокировкой нескольких потоков. Хочется предупредить о существующем нюансе и рассказать об интересном случае из поддержки наших пользователей. Причем тут поддержка? Это вы узнаете из статьи. Приятного чтения.

Предыстория

В дистрибутиве PVS-Studio есть одна утилита под названием CLMonitor.exe, или система мониторинга компиляции. Она предназначена для "бесшовной" интеграции статического анализа PVS-Studio для языков C и C++ в любую сборочную систему. Сборочная система должна использовать для сборки файлов один из компиляторов, поддерживаемых анализатором PVS-Studio. Например: gcc, clang, cl, и т.п.

Стандартный сценарий работы данной Windows утилиты очень простой, всего 3 шага:

  1. Выполняем 'CLMonitor.exe monitor';

  2. Выполняем сборку проекта;

  3. Выполняем 'CLMonitor.exe analyze'.

Первый шаг запускает 'сервер', который начинает отслеживать все процессы компиляторов в системе до тех пор, пока его не остановят. Как только мы запустили сервер выполняем сборку проекта, который мы хотим проанализировать. Если сборка прошла, то нужно запустить анализ. Для этого мы исполняем третий шаг. 'CLMonitor.exe analyze' запускает 'клиент', который говорит серверу: "Всё, хватит, выключайся и давай сюда результаты мониторинга за процессами". В этот момент сервер должен завершить свою работу, а клиент запустить анализ. Подробнее о том, как внутри работает система мониторинга и зачем сервер вообще собирает процессы, мы поговорим чуть позже.

И вот в один прекрасный момент описанный сценарий не заработал, анализ просто-напросто не запустился. Ну и чтобы было поинтереснее, возникла эта проблема не у нас, а у пользователя, который обратился к нам в поддержку. У него стабильно после запуска анализа происходило десятиминутное ожидание ответа от сервера с дальнейшим выходом из программы по timeout'у. В чём причина непонятно. Проблема не воспроизводится. Да... беда. Пришлось запросить дампфайл для процесса нашей утилиты, чтобы посмотреть, что там происходит внутри.

Примечание. Проблема у пользователя возникла при использовании Windows утилиты CLMonitor.exe. Поэтому все дальнейшие примеры будут актуальны именно для Windows.

Как работает CLMonitor.exe

Чтобы лучше понять все мои дальнейшие рассуждения о проблеме пользователя, я советую не пропускать данный пункт. Здесь я расскажу о том, как происходит взаимодействие клиента и сервера.

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

Зачем мы вообще отлавливаем процессы

Как вы поняли, история начинается с того, что нужно запустить сервер, который будет отлавливать все процессы. Делаем мы это не просто так. Вообще, более удобный способ проанализировать C++ проект это прямой запуск анализатора через утилиту командной строки PVS-Studio_Cmd. У неё, однако, есть существенное ограничение она может проверять только проекты для Visual Studio. Дело в том, что для анализа требуется вызывать компилятор, чтобы он препроцессировал проверяемые исходные файлы, ведь анализатор работает именно с препроцессированными файлами. А чтобы вызвать препроцессор, нужно знать:

  • какой конкретно компилятор вызывать;

  • какой файл препроцессировать;

  • параметры препроцессирования.

Утилита PVS-Studio_Cmd узнает все необходимое из проектного файла (*.vcxproj). Однако это работает только для "обычных" MSBuild проектов Visual Studio. Даже для тех же NMake проектов мы не можем получить необходимую анализатору информацию, потому что она не хранится в самом проектном файле. И это несмотря на то, что NMake также является .vcxproj. Сам проект при этом является как бы обёрткой для другой сборочной системы. Тут в игру и вступают всяческие ухищрения. Например, для анализа Unreal Engine проектов мы используем прямую интеграцию с *Unreal Build Tool * сборочной системой, используемой "под капотом". Подробнее здесь.

Поэтому, для того чтобы можно было использовать PVS-Studio независимо сборочной системы, даже самой экзотической, у нас и появилась утилита CLMonitor.exe. Она отслеживает все процессы во время сборки проекта и отлавливает вызовы компиляторов. А уже из вызовов компиляторов мы получаем всю необходимую информацию для дальнейшего препроцессирования и анализа. Теперь вы знаете, зачем нам нужно мониторить процессы.

Как клиент запускает анализ

Для обмена данными между сервером и клиентом мы используем программный фреймворк WCF (Windows Communication Foundation). Давайте далее кратко опишем, как мы с ним работаем.

С помощью класса *ServiceHost *создается именованный канал, по которому будет происходить обмен сообщениями между процессами клиента и сервера. Вот как это выглядит на стороне сервера:

static ErrorLevels PerformMonitoring(....) {  using (ServiceHost host = new ServiceHost(                       typeof(CLMonitoringContract),                          new Uri[]{new Uri(PipeCredentials.PipeRoot)}))   {    ....    host.AddServiceEndpoint(typeof(ICLMonitoringContract),                             pipe,                             PipeCredentials.PipeName);    host.Open();         ....  }}

Обратите тут внимание на две вещи: *CLMonitoringContract *и ICLMonitoringContract.

*ICLMonitoringContract * это сервисный контракт. *CLMonitoringContract * реализация сервисного контракта. Выглядит это так:

[ServiceContract(SessionMode = SessionMode.Required,                  CallbackContract = typeof(ICLMonitoringContractCallback))]interface ICLMonitoringContract{  [OperationContract]  void StopMonitoring(string dumpPath = null);} [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]class CLMonitoringContract : ICLMonitoringContract{  public void StopMonitoring(string dumpPath = null)  {    ....    CLMonitoringServer.CompilerMonitor.StopMonitoring(dumpPath);  } }

Когда мы запускаем клиент, нам нужно остановить работу сервера и забрать у него все необходимые данные. Благодаря данному интерфейсу мы это и делаем. Вот как выглядит остановка сервера со стороны клиента:

public void FinishMonitor(){  CLMonitoringContractCallback сallback = new CLMonitoringContractCallback();  var pipeFactory = new DuplexChannelFactory<ICLMonitoringContract>(           сallback,            pipe,            new EndpointAddress(....));  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);}

Когда клиент выполняет метод StopMonitoring, он на самом деле выполняется у сервера и вызывает его остановку. А клиент получает данные для запуска анализа.

Всё, теперь вы, хоть немного, имеете представление о внутренней работе утилиты CLMonitor.exe.

Просмотр дамп файла и осознание проблемы

Возвращаемся к нашим баранам. Мы остановились на том, что пользователь отправил нам дамп файлы процессов. Напомню, что у пользователя происходило зависание при попытке запустить анализ процессы клиента и сервера оставались висеть, закрытия сервера не происходило. Плюс ровно через 10 минут выводилось такое сообщение:

**Интересный факт. **Откуда вообще взялись эти 10 минут? Дело в том, что мы задаем время ожидания ответа от сервера намного больше, а именно - 24 часа, как видно в примере кода, приведённом выше. Однако для некоторых операций фреймворк сам решает, что это слишком много и он успеет быстрее. Поэтому берет только часть от изначального значения.

Мы попросили у пользователя снять дамп с двух процессов (клиент и сервер) минуток через 5 после запуска клиента, чтобы посмотреть, что там происходит.

Тут небольшая пауза. Хочется быть честным по отношению к моему коллеге Павлу и упомянуть, что это именно он разобрался в данной проблеме. Я же ее просто чинил, ну и вот сейчас описываю :) Конец паузы.

Дамп 'клиента'

Так вот, когда мы открыли дамп файл клиента, перед глазами предстал следующий список потоков:

Нас интересует главный поток. Он висит на методе, отвечающем за запрос остановки сервера:

public void FinishMonitor(){  ....  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);            // <=  ....}

Получается, что клиент попросил сервер завершить работу, а ответа не последовало. И это странное поведение, так как обычно данная операция занимает доли секунды. А я напомню, что в данном случае дамп был снят спустя 5 минут от начала работы клиента. Что ж, давайте смотреть, что там на сервере происходит.

Дамп 'сервера'

Открываем его и видим следующий список потоков:

Воу-воу, откуда так много TraceEvent'ов? Кстати, на скриншоте не уместилось, но всего их более 50. Ну давайте подумаем. Данный метод у нас используется, чтобы логировать различную информацию. Например, если отловленный процесс является компилятором, который не поддерживается, произошла ошибка считывания какого-либо параметра процесса и т.д. Посмотрев стеки данных потоков, мы выяснили, что все они ведут в один и тот же метод в нашем коде. А метод этот смотрит, является ли отловленный нашей утилитой процесс компилятором или это нечто иное и неинтересное, и, если мы отловили такой неинтересный процесс, мы это логируем.

Получается, что у пользователя запускается очень много процессов, которые, конкретно для нас, являются 'мусором'. Ну допустим, что это так. Однако данная картина все равно выглядит очень подозрительно. Почему же таких потоков так много? Ведь, по идее, логирование должно происходить быстро. Очень похоже на то, что все эти потоки висят на какой-то точке синхронизации или критической секции и чего-то ждут. Давайте зайдем на ReferenceSource и посмотрим исходный код метода TraceEvent.

Открываем исходники и действительно видим в методе TraceEvent оператор lock:

Мы предположили, что из-за постоянной синхронизации и логирования накапливается большое количество вызовов методов TraceEvent, ждущих освобождения TraceInternal.critSec. Хм, ну допустим. Однако это пока не объясняет, почему сервер не может ответить клиенту. Посмотрим еще раз в дамп файл сервера и заметим один одинокий поток, который висит на методе DiagnosticsConfiguration.Initialize:

В данный метод мы попадаем из метода NegotiateStream.AuthenticateAsServer, выполняющего проверку подлинности со стороны сервера в соединении клиент-сервер:

В нашем случае клиент-серверное взаимодействие происходит с помощью WCF. Плюс напоминаю, что клиент ждет ответ от сервера. По этому стеку очень похоже, что метод DiagnosticsConfiguration.Initialize был вызван при запросе от клиента и теперь висит и ждет. Хм... а давайте-ка зайдем в его исходный код:

И тут мы замечаем, что в данном методе имеется критическая секция, да еще и на ту же самую переменную. Посмотрев, что вообще такое этот critSec, увидим следующее:

Собственно, у нас уже есть достаточно информации, чтобы подвести итоги.

Интересный факт. Изучая просторы интернета в поисках информации про данную проблему с TraceEvent была обнаружена интересная тема на GitHub. Она немного о другом, но есть один занимательный комментарий от сотрудника компании Microsoft:

"Also one of the locks, TraceInternal.critSec, is only present if the TraceListener asks for it. Generally speaking such 'global' locks are not a good idea for a high performance logging system (indeed we don't recommend TraceSource for high performance logging at all, it is really there only for compatibility reasons)".

Получается, что команда Microsoft не рекомендует использовать компонент трассировки выполнения кода для высоконагруженных систем, но при этом сама использует его в своём IPC фреймфорке, который, казалось бы, должен быть надёжным и устойчивым к большим нагрузкам...

Итоги изучения дампов

Итак, что мы имеем:

  1. Клиент общается с сервером с помощью фреймворка WCF.

  2. Клиент не может получить ответа от сервера. После 10 минут ожидания он падает по тайм-ауту.

  3. На сервере висит множество потоков на методе TraceEvent и всего один - на Initialize.

  4. Оба метода зависят от одной и той же переменной в критической секции, притом это статическое поле.

  5. Потоки, в которых выполняется метод TraceEvent, бесконечно появляются и из-за lock не могут быстро сделать свое дело и исчезнуть. Тем самым они долго не отпускают объект в lock.

  6. Метод Initialize возникает при попытке клиента завершить работу сервера и висит бесконечно на lock.

Из этого можно сделать вывод, что сервер получил команду завершения от клиента. Чтобы начать выполнять метод остановки работы сервера, необходимо установить соединение и выполнить метод Initialize. Данный метод не может выполниться из-за того, что объект в критической секции держат методы TraceEvent, которые в этот момент выполняются на сервере. Появление новых TraceEvent'ов не прекратится, потому что сервер продолжает работать и отлавливать новые 'мусорные' процессы. Получается, что клиент никогда не получит ответа от сервера, потому что сервер бесконечно логирует отловленные процессы с помощью TraceEvent. Проблема найдена!

Важно тут то, что объект в *критической секции *является статической переменной. Из-за этого ошибка будет повторяться до тех пор, пока экземпляры логгеров существуют в одном процессе. При этом неважно, что и у нас, и внутри WCF используются разные экземпляры логгеров казалось бы, независимые друг от друга объекты создают взаимную блокировку из-за статической переменной в критической секции.

Теперь остается только воспроизвести и починить проблему.

Воспроизведение проблемы

Само воспроизведение получается крайне простое. Все, что нам нужно, это сделать так, чтобы сервер постоянно что-то логировал. Потому создаем метод с говорящим названием CrazyLogging, который и будет это делать:

private void CrazyLogging(){  for (var i = 0; i < 30; i++)  {    var j = i;    new Thread(new ThreadStart(() =>    {      while (!Program.isStopMonitor)        Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());    })).Start();  }}

За работу сервера у нас отвечает метод Trace, поэтому добавляем наше логирование в него. Например, вот сюда:

public void Trace(){  ListenersInitialization();  CrazyLogging();  ....}

Готово. Запускаем сервер (я буду это делать с помощью Visual Studio 2019), приостанавливаем секунд через 5 процесс и смотрим что у нас там с потоками:

Отлично! Теперь запускаем клиент (TestTraceSource.exe analyze), который должен установить связь с сервером и остановить его работу.

Запустив, мы увидим, что анализ не начинается. Поэтому опять останавливаем потоки в Visual Studio и видим ту же самую картину из дамп файла сервера. А именно появился поток, который висит на методе DiagnosticsConfiguration.Initialize. Проблема воспроизведена.

Как же её чинить? Ну для начала стоит сказать, что TraceSource это класс, который предоставляет набор методов и свойств, позволяющих приложениям делать трассировку выполнения кода и связывать сообщения трассировки с их источником. Используем мы его потому, что сервер может быть запущен не приаттаченным к консоли, и консольное логирование будет бессмысленно. В этом случае мы логировали всё в Event'ы операционной системы с помощью метода TraceSource.TraceEvent.

Проблему мы "решили" следующим образом. По умолчанию теперь вся информация логируется в консоль, используя метод Console.WriteLine. В большинстве случаев, даже если эта логируемая информация и будет потеряна из-за не приаттаченной консоли, она всё равно не требуется для решения задач в работе утилиты, а проблема исчезла. Плюс изменения заняли какие-то минуты. Однако мы оставили возможность старого способа логирования с помощью специального флага enableLogger.

Код, воспроизводящий проблему

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

Чтобы запустить имитирование работы сервера, запустите .exe с флагом trace. Чтобы запустить клиент, воспользуйтесь флагом analyze.

**Примечание: **количество потоков в методе CrazyLogging следует подбирать индивидуально. Если проблема у вас не воспроизводится, то попробуйте поиграться с этим значением. Также можете запустить данный проект в Visual Studio в режиме отладки.

Точка входа в программу:

using System.Linq;namespace TestTraceSource{  class Program  {    public static bool isStopMonitor = false;    static void Main(string[] args)    {      if (!args.Any())        return;      if (args[0] == "trace")      {        Server server = new Server();        server.Trace();      }      if (args[0] == "analyze")      {        Client client = new Client();        client.FinishMonitor();      }    }    }}

Сервер:

using System;using System.Diagnostics;using System.ServiceModel;using System.Threading;namespace TestTraceSource{  class Server  {    private static TraceSource Logger;    public void Trace()    {      ListenersInitialization();      CrazyLogging();      using (ServiceHost host = new ServiceHost(                          typeof(TestTraceContract),                           new Uri[]{new Uri(PipeCredentials.PipeRoot)}))      {        host.AddServiceEndpoint(typeof(IContract),                                 new NetNamedPipeBinding(),                                 PipeCredentials.PipeName);        host.Open();        while (!Program.isStopMonitor)        {          // We catch all processes, process them, and so on        }        host.Close();      }      Console.WriteLine("Complited.");    }    private void ListenersInitialization()    {      Logger = new TraceSource("PVS-Studio CLMonitoring");      Logger.Switch.Level = SourceLevels.Verbose;      Logger.Listeners.Add(new ConsoleTraceListener());      String EventSourceName = "PVS-Studio CL Monitoring";      EventLog log = new EventLog();      log.Source = EventSourceName;      Logger.Listeners.Add(new EventLogTraceListener(log));    }    private void CrazyLogging()    {      for (var i = 0; i < 30; i++)      {        var j = i;        new Thread(new ThreadStart(() =>        {          var start = DateTime.Now;          while (!Program.isStopMonitor)            Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());        })).Start();      }    }   }}

Клиент:

using System;using System.ServiceModel;namespace TestTraceSource{  class Client  {    public void FinishMonitor()    {      TestTraceContractCallback сallback = new TestTraceContractCallback();      var pipeFactory = new DuplexChannelFactory<IContract>(                                сallback,                                new NetNamedPipeBinding(),                                new EndpointAddress(PipeCredentials.PipeRoot                                                   + PipeCredentials.PipeName));      IContract pipeProxy = pipeFactory.CreateChannel();      pipeProxy.StopServer();      Console.WriteLine("Complited.");        }  }}

Прокси:

using System;using System.ServiceModel;namespace TestTraceSource{  class PipeCredentials  {    public const String PipeName = "PipeCLMonitoring";    public const String PipeRoot = "net.pipe://localhost/";    public const long MaxMessageSize = 500 * 1024 * 1024; //bytes  }  class TestTraceContractCallback : IContractCallback  {    public void JobComplete()    {      Console.WriteLine("Job Completed.");    }  }  [ServiceContract(SessionMode = SessionMode.Required,                    CallbackContract = typeof(IContractCallback))]  interface IContract  {    [OperationContract]    void StopServer();  }  interface IContractCallback  {    [OperationContract(IsOneWay = true)]    void JobComplete();  }  [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]  class TestTraceContract : IContract  {    public void StopServer()    {      Program.isStopMonitor = true;    }  }}

Вывод

Будьте осторожны со стандартным методом TraceSource.TraceEvent. Если у вас теоретически возможно частое использование данного метода в программе, то вы можете столкнуться с подобной проблемой. Особенно если у вас высоконагруженная система. Сами разработчики в таком случае не рекомендуют использовать всё, что связано с классом TraceSource. Если вы уже сталкивались с чем-то подобным, то не стесняйтесь рассказать об этом в комментариях.

Спасибо за просмотр. Незаметно рекламирую свой Twitter.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikolay Mironov. How WCF Shoots Itself in the Foot With TraceSource.

Подробнее..

Kanban команды PVS-Studio. Часть 1 agile

31.03.2021 16:16:13 | Автор: admin

Эта статья могла появиться на свет гораздо раньше, примерно на год. Ведь около года назад мы в компании PVS-Studio решили, что пришло время поэкспериментировать с agile. Но хотелось накопить пользовательский опыт, собрать статистику и уже потом поведать об этом миру. К тому же, одновременно с agile мы запланировали переход на другой трекер задач (вместо Bitbucket), а также много других изменений в наших внутренних процессах разработки. В общем, до написания статьи руки никак не доходили.

Кстати, про переход на другой трекер будет вторая статья. А в этой я кратко познакомлю вас с историей нашей компании, расскажу о предпосылках перехода на kanban, административных и технических трудностях, с которыми нам пришлось столкнуться, и почему же, собственно, была выбрана эта методология. В общем, хочется поделиться практическим опытом, который, возможно, будет полезен другим компаниям.

Несмотря на то, что здесь будет про управление проектами, я не планирую глубоко погружаться в теорию. Статья рассчитана на подготовленного читателя, знакомого с понятием kanban. Если же вы только слышали про эту методологию и сейчас хотите узнать о ней подробнее, то вы легко найдёте в сети множество публикаций по этой тематике.

Читая сторонние статьи на тему управления проектами, я обычно испытываю чувство глубокого уважения. Все эти статьи круты. Авторы умело оперируют терминологией, рассматривают практические кейсы, а еще успешно продают идеи другим. Сразу хочется бежать и что-то такое же внедрять у себя, чтобы эффективно, чтобы "лучшие практики". Но есть известная разница между теорией и практикой. В теории часто замалчиваются неудобные моменты, связанные со встраиванием передовых технологий в существующие рабочие процессы. Отсутствие необходимых ресурсов, неквалифицированный менеджмент, незрелость и сопротивление команды. Примерно таким был наш путь к agile: непростым, но увлекательным.

Немного истории

Компания PVS-Studio имеет более чем десятилетнюю историю развития. Все это время работы ведутся над одноименным статическим анализатором кода. Подробнее про это можно почитать в статье "Как 10 лет назад начинался проект PVS-Studio". Таким образом, PVS-Studio фактически компания одного проекта. И это проект для разработчиков. Но мы внутри компании в качестве проектов привыкли также выделять и другие направления работы, которые фиксируются в трекере задач наравне с разработкой: маркетинг, продажи, внутренние интернет-проекты, административная и IT-поддержка офиса. Все это создает достаточно большой поток разнородных задач, с которым нужно как-то справляться.

До перехода на новый трекер мы использовали Bitbucket. Первая задача там датирована 30 июня 2014, а последняя 5 февраля 2021 (примерно с этого момента мы начали использовать новый инструмент, про это будет вторая часть статьи). Всего за это время было создано 5527 задач. До Bitbucket в компании использовали еще какой-то инструмент, но не будем забираться так далеко в прошлое. Примерный подсчет дает около 3.3 новых задач на один рабочий день (при условии 250 рабочих дней в году и 24 рабочих дней в месяце). Давайте округлим это число до целого, не забыв, что не все задачи были доведены до состояния "Решено". Итого мы получим три новых задачи в день на примерно 30 сотрудников компании (с 2014 по 2021 год размер компании увеличился вдвое: с 20 до 40 человек). Вероятно, можно считать, что также и закрывали примерно три задачи в день. Предлагаю не анализировать это число, так как оно ничего не говорит о числе задач, находящихся у сотрудника единовременно в работе. Вот такой показатель был бы нам полезен. Но, к сожалению, получить его можно только экспериментально и при работе с определенной методологией, о чём будет рассказано далее.

Чуть выше я упомянул о росте команды. Очевидно, что рост команды приводит к увеличению потока задач и одновременному усложнению механизмов управления проектами. Конечно, есть масса других оказывающих влияние факторов, например структура и разнородность проектов (тоже наш случай), техническая сложность, квалификация менеджеров и исполнителей и т.п. Но, при прочих равных условиях, размер команды тут все же имеет решающее значение. Да, у нас не было ситуаций, когда в штат сразу брали много новых сотрудников, с нуля открывали какие-то большие направления и под них набирали команду. Поэтому всегда было время подготовиться и продумать план дальнейших действий. Тем не менее, именно рост команды привел нас к необходимости задуматься над изменениями в структуре компании, выборе более эффективной методики управления проектами.

Несмотря на то, что в нашем случае развитие компании шло достаточно планомерно, мы, как и многие другие, столкнулись с типичным паттерном проблемы: "Еще год назад это работало (хватало), а теперь не работает (не хватает)". Здесь и площадь офиса, и число баночек газировки в холодильнике, и максимум доступных процессорных потоков на сборочном сервере, и пр. Такая же ситуация с организацией работы и управлением. Кажется, еще совсем недавно мы обходились без регулярных one-to-one. Повышение разработчика происходило по принципу "вроде норм кодит". А фраза "верни задачу в бэклог на kanban" могла быть расценена в лучшем случае как неумелый троллинг. Конечно, все это обычная ситуация роста, и говорить о большой проблеме тут излишне сгущать краски. Тем не менее, на мой взгляд, от готовности команды и руководства компании к переменам в таких ситуациях зависит успех бизнеса.

С какими же конкретно проблемами мы столкнулись при увеличении размера компании и росте потока задач? Основная сильная централизация управления процессом разработки. К концу 2019 года вся разработка в компании на всех уровнях контролировалась в основном техническим директором, а в командах не хватало тимлидов. Также не вполне понятен был жизненный цикл задачи, неполно отслеживалась эффективность работы и сложно прогнозировался ее результат. В штате PVS-Studio на тот момент состояло около 34 человек: команды маркетинга и продаж; команды разработки на языках C/С++, C#, Java; команда DevOps, административный персонал и руководство. Примерно половина команды занималась непосредственно разработкой.

Исторически сложилось (мой любимый термин), что команды разработки у нас самоорганизовывались. И до определенного момента это работало, пока их размер не начал расти. При этом из-за фактически отсутствующего менеджмента на уровне команд общая картина для вышестоящих уровней все более размывалась. Да, были неформальные техлиды и потенциальные тимлиды, но пока было непонятно, что с ними делать.

Дополнительное негативное следствие описанной ситуации в том, что прозрачности не было и на уровне разработчиков: мало кто мог объяснить, чем же занимается соседняя команда. А иногда непонимание было даже внутри команд. Так мы столкнулись с необходимостью выбора новой современной технологии управления разработкой.

Выбор методологии

Понятно, что только внедрение какого-то нового подхода не решило бы всех наших проблем. Поэтому одновременно мы приступили к изменению структуры команд: была введена система грейдов, внутри команд явно выделены подкоманды по направлениям работы с регулярной ротацией разработчиков между направлениями (от одного релизного цикла к другому). Также мы сфокусировались на выявлении явных лидеров и подталкивании сомневающихся к лидерству. Не был забыт и трекер задач как один из важных компонентов работы: используемая система (Bitbucket) уже не справлялась с возрастающими потребностями. Переход на новый трекер был запланирован на второй этап: после выбора и пробного внедрения новой методологии.

Внедрение планировалось на действующем рабочем процессе, поэтому нужна была максимально гибкая, дружественная технология, удовлетворяющая следующим критериям:

  • простота перехода. Мы предвидели возможность естественного сопротивления команды, поэтому хотелось внедрить новые практики максимально безболезненно для рядовых сотрудников;

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

  • возможность сопровождения своими силами, несложная в освоении методология;

  • гибкость. Возможность использования для всех наших задач, даже не связанных с разработкой. Большой задел на будущее;

  • прозрачность, наглядность контроля и управления потоком задач. Годы работы с плоскими списками задач показали их низкую эффективность. Хотелось иметь визуальный инструмент, отображающий распределение задач по направлениям работы и конкретным исполнителям, позволяющий быстро выявлять проблемные места;

  • возможность получить полную актуальную картину в любой момент времени. Основная задача, решение которой вывело бы процесс управления разработкой в нашей компании на новый уровень.

После некоторых обсуждений нами была выбрана методология kanban как общее направление и философия дальнейшего развития. Выбирали мы между kanban и scrum. Но идея коротких спринтов совсем не вписывается в наши техпроцессы: у нас единственный проект в разработке, и мы привыкли работать в рамках релизного цикла (два месяца) и уже тогда подводить итоги и оценивать результаты. Также бывают срочные задачи, которые надо делать в любом случае.

Наконец, переход на scrum потребовал бы внесения значительных изменений в привычный ход работы. Преимущества kanban в том, что вначале вы просто описываете с его помощью те процессы, которые у вас есть, ничего не меняя в них. И только затем постепенно начинаете их совершенствовать. Это делает внедрение более плавным и менее тревожит команду.

На самом деле, многое из того, что декларирует agile, мы применяли ранее. Мы проводили ежедневные митинги, хоть и не во всех командах. Команды, как я уже говорил, были достаточно самоорганизованы, а сотрудники отвечали за результат. Также сотрудники имели некоторую свободу при выборе задач, например могли заняться написанием статьи одновременно с процессом разработки, самостоятельно планируя своё рабочее время. Любой сотрудник мог поставить задачу другому, независимо от его уровня в компании (матричная структура). Наконец, у нас были подобия спринтов, привязанные к релизному циклу.

Не хватало только систематизации, ритмичности при работе с потоком задач, а также чётко прописанных правил и требований, которые бы были понятны, однозначно приняты и соблюдались каждым участником процесса. "Баланс и общение" вот практически девиз нашей команды в настоящее время. И kanban здесь подходит как нельзя лучше.

Подпольный kanban

Рассмотрев предысторию, плавно переходим к созданию нашей первой kanban-доски. Вернёмся к декабрю 2019 года. На тот момент у нас ещё не было инструмента, который бы позволил сделать электронную доску. Поэтому решили купить обычную магнитно-маркерную белого цвета и много цветной бумаги. Кстати, идея такой доски не так плоха, так как дает возможность непосредственно прикоснуться к рабочему процессу.

Доску решили какое-то время вести подпольно узким кругом менеджеров. Для этого она была установлена в закрытом кабинете с ограниченным доступом. В таком режиме планировалось работать до момента, пока не будет полностью согласован макет доски. Если вам кажется (как нам поначалу), что сделать kanban-доску достаточно просто, то вот как процесс шел у нас:

  • неделя ушла на согласование первоначального макета доски и разработку дизайна карточек. В результате остановились на классическом виде доски, где в столбцах находились стадии производственного процесса, а в строках конкретные исполнители, при этом отдельный столбец отводился под бэклог и еще небольшое пространство для "корзины";

  • по столбцам в первоначальном варианте получилось так: Backlog (общий пул задач), Assignee (ответственный, исполнитель), On hold (буфер задач исполнителя), Analyze (задача на стадии изучения), In progress (задача в работе), Check (оценка результатов, приёмка), Done (задача решена);

  • макет карточки выглядел так:

  • некоторое время потратили на выбор значения лимита незавершенной работы (WIP) для исполнителя. Логичным казалось, что у человека не может быть в работе одновременно более двух задач. Тем не менее, для начала решили остановиться на ограничении в три задачи, учитывая, что разработчики у нас могут заниматься и не связанными с разработкой активностями (написание статей, например), которые можно эффективно совмещать с разработкой. Для kanban такой подход является классическим: вначале выбирается примерное значение, которое затем уточняется в процессе работы. Вообще весь kanban это один сплошной эксперимент в попытке уменьшить значение WIP. :) Спойлер: значение WIP мы в итоге все же установили равное двум;

  • после того как доска была разлинована и на ней размещены все карточки, выяснилось, что использовать маркер для разлиновки доски плохая идея: линии стирались при перемещении карточек. Пришлось снимать все карточки и заново перелиновывать доску уже при помощи тонкой клейкой ленты зеленого цвета;

  • за следующие пару недель использования макет доски ещё несколько раз уточнялся. Добавили отдельную строку для задач-эпиков без конкретного исполнителя. Уменьшили ширину столбца "Done" и за счет этого увеличили ширину столбца для бэклога. Добавили столбец "Waiting" (задача на ожидании, например, ответа от клиента, или статья на переводе) между столбцами "Check" и "Done". Решили использовать карточки разных цветов для разделения задач по типу работ: просто задача, правка бага, эпик, написание статьи и т.п.;

  • любая правка доски обсуждалась, при этом обсуждения часто бывали достаточно эмоциональными;

  • пока доска велась скрытно, штат пополнился еще несколькими разработчиками и места в строках перестало хватать. Было решено в дальнейшем купить двустороннюю доску, но уже после ее обнародования;

  • также хочется отметить необходимость проведения ежедневных работ по поддержанию доски в актуальном состоянии: синхронизацию с трекером задач. По моим подсчетам на это тратилось не менее получаса рабочего времени в день.

Так прошло чуть больше месяца (с учетом новогодних каникул), и вот в январе 2020 года мы, наконец, решили, что пришло время обнародовать доску (на фотографии ниже видна одна сторона доски, не включающая направление C++).

Kanban в массы

В тематической литературе приводятся различные сценарии использования kanban-досок. Для продвинутых команд предлагается автономное использование, когда разработчики самостоятельно берут (вытягивают) задачи из бэклога. Бэклог пополняется по результатам совещания менеджеров, каждый из которых выступает заказчиком определенных изменений. При этом за перемещение карточек по доске отвечают сами разработчики. Такой подход полностью соответствует философии kanban, где упор делается на самоорганизацию и баланс (kanban, в отличие от scrum, не предполагает привлечения scrum-мастера).

Для команд, только внедряющих kanban (как наша), все же рекомендуется привлечение выделенного сотрудника для поддержания доски в актуальном состоянии, организации митингов и обсуждений у доски, контроля за соблюдением правил работы с методологией. Такой подход также позволяет снизить возможный негативный эффект для команды при переходе на kanban. В дополнение к этому мы решили пополнять бэклог и проводить анализ работы с доской с периодичностью в один релизный цикл (два месяца).

Итак, наша доска с актуальными задачами была представлена на всеобщее обозрение и размещена в конференц-зале. Ранее мы уже практиковали ежедневные рабочие собрания, но они не носили системный характер и проводились от случая к случаю. Теперь мы решили организовать работу по расписанию и проводить ежедневные митинги по отдельности для четырёх команд разработки (C/С++, C#, Java; DevOps). Продолжительность митинга установили не более 15 минут. В дополнение к этому в отделе маркетинга велась еще одна независимая доска, которая несколько отличалась, но также вписывалась в нашу общую концепцию работы по kanban.

Собственно, введение ежедневных митингов по расписанию, а не доска, стало причиной ожидаемого сопротивления команды. Основной вопрос был примерно такой: "Зачем нам эти митинги? Мы и так знаем, кто и что делает внутри команды, проводим свои ежедневные обсуждения. Просто трата времени какая-то. Да еще эта доска". Нам кажется, что теперь после более чем года работы по kanban мы можем дать ответы на эти и другие вопросы.

Довольно быстро мы осознали, что kanban-доска является отличным местом для проведения митингов. Идея наших митингов состоит в том, что разработчик рассказывает о своей активности в первую очередь себе (систематизируя работу, выявляя что-то упущенное или не так понятое им), а уже затем коллегам и руководителю. При этом доска помогает видеть весь поток задач (как своих, так и чужих) и понимать, что ты не перегружен, что будет чем заняться дальше (есть буфер и бэклог) и есть вот эта пухлая пачка твоих задач в последнем столбце "Done" по-настоящему осязаемый результат твоей работы за последний релизный цикл. Это действительно работает, проверено.

Мы постоянно объясняли и объясняем разработчикам, что в первую очередь митинги и доска нужны именно им. Но, конечно, она помогает и руководителям всех вышестоящих уровней в принятии решений. Пример очевидной пользы доски, который приводят во всех книжках, выявление "бутылочных горлышек", быстрое устранение проблемных мест, скопления большого числа задач на одном сотруднике. Пример менее очевидной пользы выявление малой загруженности исполнителей или работы над "удобными" задачами. Речь о ситуациях, когда в работу берётся мало задач или же, берутся не самые нужные задачи, а те, над которыми просто интереснее работать сотруднику. Такие аномалии сложнее выявлять при анализе классического списка задач, так как их легко не заметить на общем фоне.

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

Таков путь

К сожалению, полноценно поработать с физической kanban-доской нам удалось только два релизных цикла (четыре месяца). После чего весной 2020 года весь офис, в связи с известными событиями (COVID), был переведен на удалённый режим работы. Конечно, ведение доски в таких условиях оказалось невозможным, и, что характерно, мы остро почувствовали нехватку этого инструмента при проведении ежедневных митингов. Я думаю, это основной показатель полезности любого инструмента: когда ты начинаешь ощущать дискомфорт при его отсутствии.

Примерно в это время мы начали задумываться об электронной kanban-доске. Какое-то время ситуация с COVID была непонятной, мы вели онлайн-митинги в Zoom. При этом каждый участник использовал список задач Bitbucket, чтобы отчитаться о работе. Это было очень неудобно и осложняло и так непростой режим работы online.

Осенью 2020 года стало понятно, что удаленная работа затягивается. Возникла идея не просто внедрить электронный аналог kanban-доски, а действовать более радикально и полностью сменить трекер задач на более удовлетворяющий нашим требованиям на тот момент. В первую очередь, конечно, мы обратили внимание на Jira. Тем более что переход на эту систему с Bitbucket позволил бы легко перенести все ранее созданные задачи. Также мы рассматривали еще одну систему YouTrack, которая очень нравилась тимлиду нашей С++ команды (привет, Филипп). Более того, по этой системе еще летом Филипп даже делал презентацию для менеджеров.

В итоге мы определились с выбором только к декабрю 2020 года и решили переходить на YouTrack. Более подробно про то, как мы переходили на этот трекер задач, я расскажу во второй части этой статьи.

Заключение

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

Что касается вообще методологий agile и kanban, их ценности для построения рабочих процессов, сложности внедрения, могу еще раз кратко озвучить наш опыт. Главное не бойтесь экспериментировать. Ведь не зря же эти подходы называют гибкими. Я бы еще добавил, что они достаточно дружелюбны и прощают множество ошибок. Можно внедрить новые подходы максимально безболезненно и даже скрытно на первых порах, а в случае неудач потери будут невелики. И если вы, как в нашем случае, будете внедрять какую-то методологию с нуля, то полученный результат, польза, точно превысят затраты.

Спасибо за внимание и до встречи во второй части.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Khrenov. PVS-Studio Team's Kanban Board. Part 1: Agile.

Подробнее..

Новые возможности PVS-Studio по оповещению разработчиков о найденных ошибках

18.05.2021 16:06:28 | Автор: admin

В поддержку PVS-Studio часто поступают предложения от пользователей по улучшению продукта. Многие из них мы с радостью берёмся реализовывать. Одно из последних таких предложений было связано с доработкой утилиты автоматического оповещения разработчиков (Blame Notifier). Нас попросили научить ее извлекать дату/ревизию кода, на который анализатор выдал сообщение, с помощью blame информации из системы контроля версий. Такая доработка позволила расширить возможности утилиты, о которых мы и поговорим в этой статье.

С чего все началось

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

Так как у клиента большая кодовая база, мы предложили логически разделить использование статического анализатора на до и после внедрения статического анализа, временно подавив ранее найденные предупреждения при помощи нашего механизма подавления. После чего анализатор бы выдавал предупреждения только на новый или измененный код. И в дополнение к этому рассказали о внедрении Blame Notifier, чтобы сразу же оповещать всех причастных лиц о новых предупреждениях. Предложение заинтересовало клиента, но из-за того, что ему хотелось видеть в отчёте как старые, так и новые предупреждения, такой режим работы показался пользователю не очень удобным. Помимо этого, он дал обратную связь по Blame Notifier: "было бы круто, чтобы предупреждения в оповещениях были поинформативней, например добавить дату и номер ревизии".

И тут нас озарило. Ранее мы думали, что для более умного и информативного оповещения нужно создавать собственную базу данных. Отталкиваясь от неё, мы могли бы отслеживать жизненный цикл каждого предупреждения и, соответственно, определять, какие из них являются новыми. Но после замечания клиента мы пришли к выводу, что система контроля версий и есть та самая база данных, из которой можно получать всю необходимую актуальную информацию. Ведь через привязку к номерам ревизий коммитов мы можем вычислить и их привязку ко времени модификации соответствующих строк кода. Более того, мы уже достаём эту информацию через blame осталось лишь воспользоваться ей.

Мы предложили клиенту следующий вариант:

  • результирующий отчёт анализатора вы будете получать со всеми предупреждениями;

  • Blame Notifier можно настроить так, чтобы в HTML-отчёт попадали предупреждения только на свежеизменённый код. Помимо этого, предупреждения будут содержать дату и номер ревизии.

На этом мы и сошлись!

Об утилите

Предназначение Blame Notifier автоматизировать процесс оповещения разработчиков, на код которых выдал предупреждение PVS-Studio, например, после ночных сборок. Утилита позволяет формировать HTML-отчёт как для конкретного разработчика только с его предупреждениями, так и для супер-пользователя, который получает полный отчёт со всеми предупреждениями. Полный отчёт по умолчанию представляет собой сгруппированные предупреждения по разработчикам, а те, в свою очередь, отсортированы в алфавитном порядке. Такая функциональность крайне полезна, так как сразу же будет сигнализировать о новых срабатываниях анализатора всем заинтересованным лицам. Как можно догадаться из названия данной утилиты, она работает на основе blame информации, получаемой для проверяемых анализатором файлов из системы контроля версий пользователя.

Кто незнаком с данной утилитой, подробнее можно познакомиться с ней в этой статье.

Что же нового

Из blame информации, помимо имени разработчика, теперь извлекается дата и ревизия последнего изменения кода, на который ругается PVS-Studio. Дополнительная извлекаемая информация позволила нам добавить в утилиту новые опции:

  • --sortByDate (-S) позволяет формировать HTML-отчёт с отсортированными предупреждениями по дате изменения исходного кода, из-за которого было выдано предупреждение анализатора. Предупреждения для конкретной даты группируются, в свою очередь, по разработчикам.

  • --days (-d) в HTML-отчёт попадают предупреждения на код, дата изменения которого меньше N дней от даты текущего запуска утилиты.

Примечание. Извлечение даты/ревизии поддержано для следующих систем контроля версий: SVN, Git и Mercurial.

Формат HTML-отчёта утилиты по умолчанию выглядит следующим образом:

Новый HTML-отчёт, отсортированный по дате:

Как это можно применить

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

Примечание. Если вы уже используете PVS-Studio в связке с SonarQube, то описанное ниже применение будет для вас неактуальным, так как соответствующая обработка новых предупреждений в SonarQube уже встроена. А вот если вы используете PVS-Studio отдельно и нет возможности\желания воспользоваться SonarQube, то это может вас заинтересовать.

Начнем с того, что у SonarQube используется подход к качеству кода, называемый 'Clean as You Code'. Его суть в том, что разработчики должны уделять повышенное внимание надежности и безопасности нового кода, который был только-только добавлен или изменен. Старый код, который уже годами доказывает свою работоспособность в 'production', сместить на второй план и сосредоточиться на том, что происходит "сегодня", тем самым предотвращая появление новых проблем. А к давно существующим проблемам периодически возвращаться и исправлять. С этим подходом подробней можно ознакомиться в блоге разработчиков SonarQube.

Данный подход реализован следующим образом. На главной странице проекта есть выделенная область с новыми проблемами за настраиваемый промежуток времени. Для новых проблем настраивается 'Quality Gate', так называемый индикатор соответствия нового кода заданным пороговым метрикам. Например:

  • количество новых багов;

  • количество новых уязвимостей;

  • коэффициент технического долга;

  • покрытие нового кода тестами;

  • ... и другие.

Например, каждое утро будет загружаться свежий отчёт анализатора, и если пороговое значение какой-либо метрики превышено, то 'Quality Gate' будет сигнализировать вам об этом.

Визуальное представление подхода 'Clean as You Code' интуитивно понятное. Вот пример, как это выглядит в SonarQube 7.9.4:

SonarQube профессиональный инструмент, предоставляющий функциональность, которая позволяет удобно и эффективно контролировать качество программного продукта. Например:

  • разбивает проблемы в коде на существующие и свежие;

  • предоставляет различные метрики и графики;

  • позволяет фильтровать проблемы по критериям;

  • позволяет смотреть предупреждения, найденные инструментами контроля качества кода, непосредственно в проверяемом коде прямо из web-браузера;

  • ... и многое другое.

Если SonarQube уже используется, то, чтобы интегрировать с ним результаты работы анализатора PVS-Studio, вам нужно ознакомиться со статьей.

А вот что делать, если вы ещё не используете SonarQube?

Если у вас есть возможность, то можно установить его, но для этого необходимо предварительно провести процедуру развёртывания, установки и первичной настройки SonarQube, что вполне может вызвать определенные трудности. Например, при:

  • запуске сервера SonarQube;

  • настройке 'Quality Profiles';

  • настройке 'Quality Gates';

  • использовании 'sonar-scanner';

  • ... и так далее.

Однако далеко не каждой команде разработчиков требуются все возможности, предоставляемые SonarQube. И поэтому долгая и достаточно трудозатратная процедура по развёртыванию и поддержанию работы данной системы вполне может оказаться избыточной.

И здесь-то как раз на помощь приходит наша утилита Blame Notifier. Она и раньше уже могла заместить часть возможностей SonarQube в области уведомления разработчиков. С новыми же возможностями утилиты теперь можно имитировать облегченную версию подхода 'Clean as You Code', где основной метрикой качества кода будет появление новых предупреждений анализатора. В таком режиме рассылка будет содержать предупреждения на код, дата изменения которого меньше N дней от даты текущего запуска утилиты.

Повторим поведение SonarQube. Для этого укажем 10 дней для опции '-d' и отсортируем предупреждения (-S) по дате изменения кода, вызвавшего предупреждение. В таком случае HTML-отчёт будет следующим:

Как видно, отчёт содержит 11 новых предупреждений за последние 10 дней. Выглядит практически так же, как и в SonarQube, но получить такую отчётность в разы легче.

У такого "облегченного" подхода, конечно, есть и свои недостатки:

  • качество нового кода оценивается по одной метрике;

  • срабатывания на код, дата изменения которого за пределами рассматриваемого периода, не будут включены в HTML-отчёт. Для этого необходимо получить полный отчёт за все время при помощи дополнительного запуска 'blame-notifier' без ограничивающих опций.

  • ну и, конечно, отсутствует возможность навигации по проверяемому коду непосредственно с помощью web-браузера

Стоит также упомянуть, что использование утилиты Blame Notifier и PVS-Studio плагина для SonarQube доступно только в Enterprise лицензии PVS-Studio.

Заключение

Внедрение технологии статического анализа в разработку подразумевает не только запуск анализатора и получение отчёта. Обычно рука об руку со статическим анализатором идут вспомогательные утилиты/плагины, использование которых позволяет легко и удобно поддерживать качество вашего кода.

Например, PVS-Studio имеет множество плагинов (для Visual Studio, SonarQube, Jenkins, Gradle/Maven/IntelliJ IDEA), утилиту для более удобной конвертации отчётов PlogConverter, утилиту оповещения разработчиков blame-notifier. Команда PVS-Studio, опираясь на свой опыт и на обратную связь пользователей, постоянно совершенствует свой продукт. Чтобы не пропустить все изменения и идти в ногу с инструментом, не забывайте следить за блогом на официальном сайте.

Новый режим работы Blame Notifier позволяет "легковесно" выполнять часть функционала, который раньше был доступен в нашем продукте только в связке с SonarQube. Однако, хочется ещё раз повторить мы ни в коем случае не призываем отказываться от использования SonarQube. Новый режим позволяет лишь значительно проще попробовать такой подход к работе со статическим анализом. Мы верим, что для многих команд данный режим будет актуален.

Если у вас есть предложения по улучшению нашего продукта, не вздумайте откладывать это в долгий ящик и сразу же пишите нам.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Maxim Stefanov. PVS-Studio New Features for Notifying Developers About Errors Found.

Подробнее..

Эй, PVS-Studio, а где же ивенты?

12.02.2021 18:18:50 | Автор: admin

Нас часто спрашивали коллеги по бизнесу и клиенты, что же мы делали весь 2020 без мероприятий. Год без офлайн событий все переживали как могли, и мы не исключение, но даже из такого затруднительного положения есть выход.

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

Новый год новые возможности!...?

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

Бизнес ивенты

В начале года мы успели посетить лично (офлайн) некоторые конференции, среди которых:

Уже к апрелю стало происходить что-то непонятное в бизнесе - часть запланированных конференций отваливалась быстрее, чем кто-либо успевал адаптироваться под изменения. Таким образом, весь весенний офлайн сезон мы успешно пролетели. Хорошо, что коллеги по бизнесу быстро ориентировались и часть из организаторов уводили конференции в онлайн. Это помогло нашим спикерам не потратить силы зря и выступить с подготовленными докладами. И уже в мае мы стали потихоньку держаться на плаву и поддерживать активность.

Так мои коллеги выступили с докладами на конференции DevGamm online 2020 (Виктория Ханиева "Static Analysis and Unity Projects: why and how" и Георгий Грибков "Качество кода игровых движков: неужели всё так плохо?")

В июне нас поглощает онлайн, и мы пытаемся найти свою нишу, участвуя в некоторых пробных конференциях. Так мы успели засветиться на DotNext Online, C++ Russia Online и C++ on Sea (Юрий Минаев "Hypercritical C++ Code Review").

После летнего периода нам становится понятно, что онлайн вообще не наш формат. Мы решили, что оставим онлайн только для спикеров, но в качестве спонсоров больше не участвуем. Вплоть до конца года в онлайне как спикеры мы успели захватить следующие конференции:

И еще пара конференций в офлайне:

  • TestCon Moscow 2020 (Никита Липилин "Что интересного в тестировании инструмента для тестирования");

  • SQA Days 27 (Георгий Грибков "Как облегчить жизнь себе и разработчикам: статический анализ в деле (на примере Unreal Engine 4)").

Кроме конференций нашим спикерам понравились выступления в различных подкастах.

Юрий Минаев:

Филипп Хандельянц:

Подкаст с участие и Филиппа Хандельянца, и Юрия Минаева:

Из новеньких форматов мы опробовали вебинары: ITVDN - Типичные ошибки в коде на примере С++, С# и Java. Хорошая возможность подготовить спикера в подаче материала перед большими конференциями.

Конечно, в связи с меньшим количеством бизнес мероприятий мы придумывали и свои форматы. Так родилось наше детище видеомарафон. Весь процесс создания и актуальность формата вы можете прочитать здесь.

Что ж, вроде неплохо для такого "неурожайного" года, но мы и не думали на этом останавливаться. БОльшую часть прошлого года мы посвятили своей команде, её сплочению, развитию, поддержанию и т. д., поэтому мы плавно переходим к следующему разделу.

Внутренние мероприятия

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

Думаю, не мне вам рассказывать про сложности с удаленным общением, взаимодействием, постоянно нарастающим стрессом и выгоранием. В зависимости от появления в нашем коллективе той или иной проблемы уже в существенных масштабах, мы устраивали выездные сессии для их решения.

Корпоративные мероприятия мы открыли с двух праздников 23 февраля и 8 марта. Тут всё как обычно, девочки готовили сюрприз мальчикам, мальчики девочкам. Ребятам мы подготовили подарочки и угощения, а 8 марта они решили проверить нас на скорость реакции и сообразительность, организовав для нас квест.

Также в марте мы успели получить немного адреналина и съездили на картинг.

Потом было долгое карантинное бремя, кажущееся вечностью. В начале лета мы стали потихоньку возвращаться в офис, но реалии были уже совсем другими.

Когда ограничения становились менее жесткими, мы старались использовать лето по максимуму как на работе, так и за ее пределами. Осваивали новые навыки: стрельба из лука, катание на квадроциклах, байдарках, лошадях, устраивали пикники и многое другое.

Кроме какой-то летней передышки, мы посвятили время внедрению новых сотрудников. Устраивали выездные мероприятия с целью ознакомления с внутренним устройством команды, но на этом мы не останавливались и осенью разъехались по командам: C++ отдел, C# отдел, Java отдел + Tools&Devops и команда маркетинга. Зачем? Всё очень просто: мы по-отдельности проходили обучение по коммуникации.

Некоторые тематические мероприятия мы тоже успели захватить. Так перед самым началом осенних карантинных мер, мы досрочно устроили вечеринку в стиле Halloween.

Что показал 2020

Давайте немного поговорим о сухой статистике. В 2019 году в общей сумме мы захватили ~40 мероприятий (36 бизнес направленности и несколько внутренних). В 2020 году эта цифра упала, но не сильно. Мы успели поучаствовать в 23 бизнес мероприятиях и устроили 13 внутренних. Итого 36 ивентов.

Да, из диаграммы видно, что бизнес мероприятий стало куда меньше, и мы компенсировали их внутренними событиями. Нельзя сказать, что команда была такой проблемной, что нам понадобилось аж столько выездов :D Нет! Внутренние мероприятия перекрывали множество целей, которые потом максимально помогли в развитии сотрудников, их сплочению и продуктивной работе. Плюс, постоянные активности помогли понять, что, не вкладывая в свою команду результатов не достичь. Не покупая участие в онлайн конференциях и других ивентах/проектах, мы вложили эти средства в своё развитие, о чем не жалеем. Да, мы не смогли выполнить некоторые бизнес задачи, но это лучше, чем получить финансовые потери, выгоревшую команду и всё так же недостигнутые бизнес цели.

Заключение

2020 не приговор :D Вы же сами понимаете, что любые трудности это прежде всего точки роста. Да, бывают очень сложные случаи, но сейчас не об этом. Прошлый год перевернул практически все сферы, и все приспосабливались по-своему, внедряя новые методики, процессы, системы и т. д. Мы с головой ушли во внутреннее развитие команды, и это круто! Основной посыл этой статьи в том, что, независимо от условий, будьте креативными и инициативными. "Что-то не получается, потому что вот этот карантин/удаленка/коллега/поставщик мешает, а так бы мы уже сотни миллионов зарабатывали" - так себе оправдание, а вот попробовать увидеть не следствие, а проблему ситуации, а потом еще и несколько решений предложить. Вот это здорово! Это и помогает передавать опыт другим компаниям или получать его, придумывать новые решения, которые потом выходят на мировые рынки. Безусловно, такой взгляд на ситуации ведет к развитию, что очень ценно в современной действительности.

P.S. Все мероприятия проводились в соответствии с указами и постановлениями губернатора Тульской области. И да, у нас всё хорошо. Мы продолжаем активно и успешно работать. Желаем вам тоже побольше активностей и здоровья!

Подробнее..

Roslyn API, или из-за чего PVS-Studio очень долго проект анализировал

22.04.2021 16:18:43 | Автор: admin

Многие ли из вас использовали сторонние библиотеки при написании кода? Вопрос риторический, ведь без применения сторонних библиотек разработка некоторых продуктов затягивалась бы на очень-очень большое время, потому что для решения каждой проблемы приходилось бы "изобретать велосипед". Однако в использовании сторонних библиотек кроме плюсов имеются и минусы. Один из этих минусов недавно коснулся и анализатора PVS-Studio для C#. Анализатор долгое время не мог закончить анализ большого проекта из-за использования метода SymbolFinder.FindReferencesAsync из Roslyn API в диагностике V3083.

Жизнь в PVS-Studio, как обычно, шла своим чередом. Разрабатывались новые диагностики, улучшался статический анализатор, писались новые статьи. Как вдруг! У одного из пользователей нашего анализатора на его большом проекте в течение дня шёл анализ и никак не мог закончиться. Alarm! Alarm! Свистать всех наверх! И мы свистали, получили дампы от пользователя и начали разбираться в причинах долгого анализа. При подробном изучении проблемы выяснилось, что дольше всех работали 3 C# диагностики. Одной из них оказалась диагностика под номером V3083. К этой диагностике уже и раньше было повышенное внимание, но пора было предпринять конкретные действия. V3083 предупреждает о некорректных вызовах C# событий. Например, в коде:

public class IncorrectEventUse{  public event EventHandler EventOne;    protected void InvokeEventTwice(object o, Eventers args)  {    if (EventOne != null)    {      EventOne(o, args);              EventOne.Invoke(o, args);    }  }}

V3083 укажет на вызовы обработчиков события EventOne в методе InvokeEventTwice. О причинах опасности этого кода вы можете узнать более подробно в документации этой диагностики. Со стороны, логика работы V3083 очень простая:

  • найти вызов события;

  • проверить, корректно ли это событие вызывается;

  • выдать предупреждение, если событие вызывается некорректно.

Из-за этой простоты становится еще интереснее понять причину долгой работы диагностики.

Причина замедления

На самом же деле логика чуть-чуть сложнее. V3083 в каждом файле для каждого типа создает только одно срабатывание анализатора на событие, куда записывает номера всех строк (для навигации в различных плагинах: Visual Studio, Rider, SonarQube), где событие некорректно вызывается. Получается, первым делом необходимо найти все места вызова события. Для подобной задачи в Roslyn API уже имеется метод SymbolFinder.FindReferencesAsync, который и был использован в V3083, чтобы не "изобретать велосипед".

Этот метод советуют использовать во многих руководствах: первое, второе, третье и т. д. Возможно, в каких-то простых случаях скорости работы этого метода и достаточно. Однако, чем больше кодовая база проекта, тем дольше этот метод будет работать. На 100 % мы убедились в этом только после изменения V3083.

Ускорение V3083 после изменения

При изменении кода диагностики или ядра анализатора необходимо проверить, что ничего из того, что раньше работало, не сломалось. Для этого у нас имеются позитивные и негативные тесты на каждую диагностику, юнит тесты для ядра анализатора, а также база open-source проектов (которых уже почти 90 штук). Для чего нам база open-source проектов? На ней мы запускаем наш анализатор для проверки его в "боевых условиях", а также этот прогон служит дополнительной проверкой, что мы ничего не сломали в анализаторе. У нас уже имелся прогон анализатора на этой базе до изменения V3083. Все, что нам осталось сделать, это совершить аналогичный прогон после изменения V3083 и выяснить выигрыш во времени. Результаты нас приятно удивили. Без использования SymbolFinder.FindReferencesAsync в V3083 мы получили ускорение на тестах на 9 %. Если кому-то эти цифры показались незначительными, то вот вам характеристики компьютера, на котором производились замеры:

Думаю, после этого даже самые упертые скептики убедились в масштабах проблемы, которая тихо-мирно жила в диагностике V3083.

Заключение

Пусть всем, кто использует Roslyn API, эта заметка будет предостережением! И вы не допустите наших ошибок. Причем, это касается не только метода SymbolFinder.FindReferencesAsync, но и всех других методов класса Microsoft.CodeAnalysis.FindSymbols.SymbolFinder, которые используют один и тот же механизм.

Также советую всем разработчикам тщательней и внимательней изучать библиотеки, которые вы используете. Я говорю это не просто так! Чтобы понять, почему это так важно, советую изучить две других наших заметки: первая, вторая. В них этот вопрос рассмотрен более подробно.

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

Изменение диагностики V3083 пока что не попало в релиз, поэтому версия анализатора 7.12 работает с использованием SymbolFinder.FindReferencesAsync.

Как упоминалось ранее, замедление анализатора было найдено еще в двух C# диагностиках, кроме V3083. Ради интереса, предлагаю читателям оставлять свои предположения о том, какие это диагностики в комментариях. Когда предположений станет больше 50, я приоткрою завесу тайны и назову номера этих диагностик.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valery Komarov. Roslyn API: Why PVS-Studio Was Analyzing the Project So Long.

Подробнее..

PVS-Studio вступает в битву с захардкоженными паролями

02.03.2021 18:23:53 | Автор: admin

PVS-Studio статический анализатор, позволяющий обнаружить множество проблем, скрытых в исходном коде. Среди них также присутствуют ошибки, связанные с безопасностью приложений. К примеру, недавно анализатор научился определять наличие в коде конфиденциальных данных, таких как пароли. Данная потенциальная уязвимость находится в списке OWASP Top Ten и является куда более опасной, чем может показаться на первый взгляд. В чём же эта опасность состоит и как статический анализатор может от неё уберечь? Что ж, об этом (и не только) и написана данная заметка!

Мы продолжаем развивать PVS-Studio как SAST решение и планируем научить анализатор находить ещё больше ошибок, связанных с безопасностью, в коде на C, C++, C# и Java. Более подробно об этих планах (и не только) можно прочитать в статье "Дорожная карта PVS-Studio на 2021 год".

О хранении секретных данных в коде

Одним из вариантов развития поддержки SAST является добавление новых диагностических правил, реализующих проверку соответствия кода различным стандартам. Среди последних нововведений в C#-анализаторе стала проверка наличия в исходниках конфиденциальных данных. Хранение таких данных в коде противоречит пункту 2.10.4 OWASP Application Security Verification Standard (ASVS):

Verify passwords, integrations with databases and third-party systems, seeds and internal secrets, and API keys are managed securely and not included in the source code or stored within source code repositories. Such storage SHOULD resist offline attacks. The use of a secure software key store (L1), hardware TPM, or an HSM (L3) is recommended for password storage.

Риски, связанные с небезопасным хранением конфиденциальных данных в коде, входят в список OWASP Top Ten. В Common Weakness Enumeration (CWE) также присутствуют 2 позиции, связанные с данным вопросом: CWE-798 и CWE-259. Несмотря на это может возникать вопрос в чём же состоит опасность?

Для проектов с открытым исходным кодом ответ очевиден пароль или какие-либо другие данные, записанные в коде, доступны всем желающим для просмотра и использования. Злоумышленнику даже не придётся совершать каких-либо сложных действий: достаточно просто покопаться в репозитории.

Несколько лучше дела обстоят в том случае, если приложение доступно только в скомпилированном виде. Это даже может создать иллюзию безопасности. Ведь исходный код, казалось бы, недоступен, а значит, недоступны и записанные в нём данные. Увы, это необязательно так.

В реальной практике нередкими являются случаи, когда в системе захардкожены данные, которые могут использоваться для получения различных прав. Как правило, у пользователей даже нет возможности изменить эти данные. Для их получения злоумышленники могут использовать самые разные способы. В некоторых случаях логины, пароли и т. п. вообще можно увидеть в интерфейсе системы. В других понадобится изучение различных файлов, декомпиляция кода, brute force и прочее. Так или иначе, умельцы умеют находить способы раскрытия захардкоженных секретов.

Достаточно часто актуальна следующая проблема: злоумышленник, получив хранящиеся в исходниках системы логины и/или пароли, сможет использовать их для подключения к другим системам этого типа. К примеру, он может установить систему локально. Проведя исследование и получив логины и пароли для этой локальной версии, злоумышленник сможет подключаться и к другим, использующим те же данные.

Кроме того, потенциальную опасность представляет тот факт, что данные, хранящиеся в исходниках, доступны всем программистам, которые с ними работают. В то же время, пользователь, установивший ту или иную систему для своих нужд, едва ли будет рад узнать, что компания-разработчик в любой момент может получить полный контроль над используемой им системой а следовательно, получить различные секретные данные самого пользователя и т. д. Найденные в списке Common Vulnerabilities and Exposures (CVE) вхождения говорят о том, что такие ошибки рано или поздно обнаруживаются. И при этом, конечно же, выставляются на всеобщее обозрение.

Как было сказано ранее, уязвимости, связанные с захардкоженными конфиденциальными данными, не редкость: среди CVE можно отыскать множество примеров. Один из них CVE-2012-5862. Система, о которой сообщается в данной позиции CVE, содержала файл "login.php", в котором присутствовал следующий код:

$password = mysql_escape_string($_POST['password']);if (crypt($password,salt)=='satIZufhIrUfk'){  $sql_pthr_ = "SELECT user,password FROM account WHERE livello = 0";  ....}if ($password=='astridservice' and $stilecustumization=='astrid'){ // <=  ....}if (crypt($password,salt)=='saF8bay.tvfOk'){  $sql_insert="INSERT INTO account(user,password,livello,nome) VALUES                 ('sinapsi','sinapsi','0','Amministratore Sinapsi')";  ....}

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

C#-анализатор PVS-Studio обнаруживает хранение конфиденциальных данных с помощью диагностического правила V5601. К примеру, взгляните на C#-код, напоминающий вышеприведённый пример:

string password = request.GetPostValue("password");....if (password == "astridservice" && stilecustomization == "astrid") ....

Проанализировав данный код, PVS-Studio сформирует следующее предупреждение:

V5601 Suspicious string literal could be a password: 'astridservice'. Storing credentials inside source code can lead to security issues.

Таким образом, статический анализатор позволит вовремя заметить подобную ошибку в коде и внести исправление. Следовательно, уровень безопасности вашего проекта также возрастёт.

Примечание. Стоит отметить, что V5601 принадлежит группе диагностических правил OWASP. Данная группа появится в PVS-Studio с выходом версии 7.12. По умолчанию OWASP-правила будут отключены, однако это можно легко изменить, используя, к примеру, интерфейс плагина для Visual Studio или Rider либо же напрямую редактируя файл настроек.

Указанный пример лишь один из многих: захардкоженные конфиденциальные данные могут привести к появлению самого разного рода проблем. Во время своего исследования я находил множество других CVE-позиций, связанных с захардкоженными конфиденциальными данными. Ниже приведены ссылки на некоторые из них:

  • CVE-2004-1920 роутер с super-логином и super-паролем;

  • CVE-2004-2556 точка доступа с super-логином (опять) и не-super-паролем "5777364";

  • CVE-2004-2557 результат "исправления" CVE-2004-2556 (по крайней мере, логин больше не super);

  • CVE-2012-1288 захардкоженные данные аккаунта администратора;

  • CVE-2012-2949 захардкоженный пароль в приложении на Android

  • и т. д.

Ещё один повод запускать анализ регулярно

Существует расхожее мнение, что статический анализатор достаточно использовать раз в несколько месяцев перед релизом (или вообще раз в год). Это достаточно странная позиция. Исправить проблемы, накопившиеся за кучу времени, куда труднее, чем поправить перед коммитом код, написанный только что. Тем более, благодаря инкрементальному анализу проверка пройдет гораздо быстрее.

Во многих случаях удобным вариантом будет настройка анализа коммитов и pull request'ов. Это ещё сильнее повысит безопасность разрабатываемого приложения. Ведь код, содержащий ошибки, никак не попадёт в основную ветку репозитория. Это будет настоящим спасением в том случае, если разработчик вдруг забыл провести анализ. Более подробно про то, как настроить проверку pull request'ов, можно почитать в документации (см. раздел "Развёртывание анализатора в облачных CI").

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

Если же анализ проводится редко, то выходит, что захардкоженные данные будут долгое время храниться в исходниках. Для open-source проекта это совсем плохо к тому времени, как анализатор позволит обнаружить проблему, данные уже нельзя будет считать конфиденциальными. Тем не менее, не защищены от подобной ситуации и другие проекты. Что если пользователь получит, скажем, бета-версию приложения? Такую как раз могут выдать между релизами. Если регулярные проверки исходников не проводятся, код в такой версии не будет проверен статическим анализатором. Получается, что все данные, "спрятанные" в исходниках, опять оказываются в открытом доступе.

Заключение

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

А как насчёт того, чтобы попробовать использовать PVS-Studio на своих проектах? Сделать это можно совершенно бесплатно, перейдя по ссылке и заполнив простенькую формочку. Ну, а у меня на этом всё, спасибо за внимание и до новых встреч!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Lipilin. PVS-Studio Clashes with Hardcoded Passwords.

Подробнее..

Как делался новый дизайн сайта PVS-Studio

04.06.2021 18:05:38 | Автор: admin

Сайту PVS-Studio в этом году исполнится 15 лет. Это солидный возраст для любого интернет-ресурса. Далёкий 2006-й в России был признан годом гуманитарных наук. В июне появилась никому не знакомая тогда площадка "Хабрахабр". В ноябре Microsoft официально завершила разработку ОС Windows Vista. И в том же месяце был зарегистрирован домен viva64.com.

10-ый юбилей наш сайт отметил редизайном. С тех пор происходящие изменения касались, скорее, размера ресурса и его функционала, но никак не дизайна. За это время количество статей увеличилось настолько, что пришлось вводить теги для удобной навигации по ним. Также сейчас активно развивается наш YouTube-канал, а это значит, что полнится запас видео и на сайте тоже. Количество страниц растёт с огромной скоростью. А вот удобство пользования остаётся там, где и было.

Настало время масштабных изменений!

С чего мы начали

Первое, что нужно понять при любых глобальных изменениях, зачем они нужны (и нужны ли вообще). С этого мы и начали. Редизайн назревал давно. И вот, что нас к нему толкало:

  1. Комментарии к статьям, в которых люди искренне удивлялись, что, ОКАЗВАЕТСЯ, у нас есть триальный ключ. И его можно запросить. И сделать это можно достаточно легко на нашем сайте.

  2. Иногда нам задавали вопросы, где же посмотреть изменения по последнему релизу.

Про версию для Linux спрашивают до сих пор. Хотя, будем честны, вопросов стало всё же меньше.

  1. Вебвизор показывал нам мучения пользователя, оказавшегося на странице загрузки.

  2. Ну и в целом, что-то нам подсказывало, что длинный текст на странице с описанием продукта не всегда и не все читают (ваши вопросы в фидбеке нам это подсказывали, конечно же).

  3. Также были сложности с рекламой и посадочными страницами под неё. Не всем было интуитивно понятно, что для запроса триала нужно немного пролистать открывшуюся страницу, заполнить форму (выбрать платформу, версию триала, внести почту, имя, фамилию, добавить комментарий (недавно мы сделали это поле необязательным!)...).

Именно от этого мы и стали отталкиваться при прототипировании нового сайта.

Прототип

Начинать прототипирование любого ресурса необходимо с формулировки цели. Если это информационный ресурс, обычно цель возвраты и время на сайте. Если интернет-магазин, цель покупка. В нашем случае (для продуктового сайта) цель запрос триала и запрос цены. Лид, если быть точнее.

Статистика показывает, что до запроса триала пользователь должен посмотреть в среднем 3-4 страницы. Почитать, возможно, статьи, перейти на страницу о продукте, посмотреть разные виды лицензий, выяснить, куда интегрируется анализатор, и только после этого, возможно, скачать дистрибутив и запросить триал.

К чему мы пришли:

  1. Форма запроса триала должна быть на каждой странице, и она должна быть проще. Ещё проще! Совсем простой!

  2. Аналогично в прямом доступе на каждой странице должна быть возможность перейти к скачиванию дистрибутива.

  3. Возможность скачать дистрибутив должна быть реализована по-разному (если кому-то по тем или иным причинам не подошёл один вариант, он мог бы всегда воспользоваться другим).

  4. Выбор трёх платформ должен быть максимально заметен. Нельзя, чтобы кто-то мог просмотреть наличие версий для Linux и MacOS.

  5. Преимущества продукта должны быть наглядными и краткими.

  6. Основные страницы сайта должны давать ответы на главные вопросы пользователей.

  7. Публикации и документация должны быть вынесены отдельно, чтобы работа с ними была максимально удобной. При этом нужно не потерять продуктовую страницу.

Какие решения мы придумали?

  1. Сократили форму триала до нескольких пунктов и сделали ее сквозной по всему сайту.

  2. Добавили в шапку кнопку "Скачать дистрибутив", закрепили шапку при прокрутке страницы.

  3. Сделали разные формы скачивания дистрибутива на странице продукта и загрузки.

  4. Выделили шаги по скачиванию дистрибутива и первым шагом вынесли выбор платформы (Windows, Linux, MacOS).

  5. Сформулировали кратко 10 основных преимуществ статического анализатора кода PVS-Studio, сделали этот блок раскрывающимся.

  6. Для каждой страницы расписали цели. Исходя из этого разбили страницы на блоки, максимально закрывающие сформулированные цели.

  7. Создали отдельные шаблоны для публикаций и документации. Однако объединили все одной темой и не стали дробить на разные домены.

Дизайн

Как корабль назовёшь, так он и поплывёт. В случае в веб-ресурсом: какой дизайн сайту придумаешь, такой и жди результат. Действительно, от дизайна зависит многое. Запомнится ресурс или нет. Найдутся все полезные элементы на нём или нет. Сбежит ли пользователь с сайта после загрузки страницы или нет...

Поэтому дизайну мы были готовы уделить максимум внимания. Нашли стороннего исполнителя с хорошим портфолио. Неважно, что он был далеко от нас (в другом городе). 2 недели мы ждали, пока договоры путешествовали между городами. Потом ещё 2 недели дизайнер готовил для нас свой вариант главной страницы. Наконец, результат был у нас. Решающий момент и слёзы огорчения. Это было совсем не то, что нам хотелось.

Как правильно говорят: без внятного ТЗ результат будет... таким же невнятным. Мы учли наши ошибки и расписали максимально подробно все правки, которые хотелось бы внести в полученный вариант. По пунктам. С примерами. Видимо, перестарались. В ответ нам пришло письмо:

Так, спустя месяц, мы остались без дизайнера.

Искать нового, опять тратить время на подписание договоров и надеяться, что этот-то уж от нас не откажется по этому пути мы решили больше не ходить. Хватит. Пройдёмся по нему с верстальщиком (спойлер).

Пришли к нашему штатному дизайнеру Галине с повинной. Интересно, почему не пришли сразу? Да потому что Галя всегда завалена другими задачами от нас. Она нам и рисует, и анимирует, и печатает, и обрабатывает, и придумывает... Многовато для одного человека, да?

Но, как бы там ни было, Галя нам не отказала. Взяла стопку наших требований:

  1. Цвета приглушенные, берем из логотипа PVS-Studio. Если оставить наш голубой и разбавить его белым, такой контраст режет глаз. Особенно плакали наши любители тёмных тем. Серый как раз решил их проблему. Да и выделил все элементы, став идеальным фоном.

  2. Единорогов оставляем, но снижаем их градус. Не все, к сожалению, ценят нашего маскота. Особенно банки, особенно серьёзные.

  3. Вместо потерянных единорогов добавляем популярную сейчас геометрию.

  4. И, чтобы не отставать от трендов, разбавляем градиентом (заметили, что у многих компаний после ребрендинга появился градиент? ;) ).

  5. Добавляем иконки, отрисовываем инфографику.

  6. Самый важный пункт делаем красиво!

Конечно, ТЗ было более детальным, но сейчас в статье расписывать всё это не имеет смысла.

Дело пошло. Единороги, геометрия, все элементы выделены. Взгляд падает туда, куда нужно.

Верстка

Думали, на дизайне мы испытали все сложности сотрудничества с внешними исполнителями? Нет, это не так. Верстальщика мы тоже искали со стороны, так как в штате у нас сейчас закоренелый бэкендер. Нашли фронтендера, подписали договор, скинули ТЗ, прототип и макет. Сели ждать.

Прождали 2 недели, просим показать хоть что-нибудь. Получаем ответ, что ещё рано. Ждём дальше.

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

По всем законам жанра мы должны были пойти с повинной к нашему штатному разработчику. Но нет. Бэкендер он и есть бэкендер. Мы всё же бросились на ускоренные поиски нового верстальщика на стороне. Грабли у Вселенной закончились. Мы быстро нашли нового фронтендера. Работа пошла.

Первым делом верстальщик попытался заманить нас на Next.js. Мы почти согласились (скорость прогрузки страниц покоряла). Да и с точки зрения SEO проблема легко решалась (поисковикам отдавался контент в html благодаря SPA). Однако недолгие раздумья привели нас к непреодолимому препятствию: кто же будет поддерживать всю эту красоту? Поэтому мы вернулись к старому доброму html+CSS+js.

Бэкенд

Новая верстка требовала серьезной переработки функционала. Благо, наш штатный разработчик подрабатывает волшебником. Поэтому он просто на готовую верстку с нуля перенес движок по блокам. Просто для нас совсем непросто для него.

Смена домена

Одновременно с новым дизайном нам хотелось перейти на новый домен. Домен, который больше не будет вызывать вопросов. Знали бы вы, как часто нам приходилось рассказывать, почему у нас сайт viva64.

Казалось бы, переход максимально прост: создаём новый сайт на новом домене, в нужный момент настраиваем редиректы с viva64.com на pvs-studio.com. Готово!

Однако поисковые системы внесли небольшие корректировки в наш идеальный план. Ведь если одновременно меняется и домен, и контент, то для них это новый сайт (что логично). А новый сайт слишком долго набирает позиции в выдаче. Поэтому пришлось отсрочить смену дизайна. Первым шагом мы совершили переход на новый домен. Отправили запрос в поисковые системы, получили от них благословение. И только после этого представили на ваш суд редизайн сайта pvs-studio.com.

В завершение

Хотя впереди еще много работы, сайт в первом приближении готов. Он ждёт своих посетителей, ценителей, любителей. А мы ждём ваши комментарии: что стало удобней, а что можно сделать ещё удобнее (уверены, что нет предела совершенству).

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Inna Pristyagina. PVS-Studio's New Website: How We Designed It.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru