Использование TAB в строковом литерале


GTK – популярный фреймворк с открытым исходным кодом для создания графических интерфейсов, который интересно проверять с помощью анализатора PVS-Studio. Тем более, что предыдущую проверку мы делали около 3 лет назад, а значит, наверняка найдём в нём новые ошибки.


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


Фрагмент N1. Escape sequence


gboolean
gtk_builder_value_from_string_type (....)
{
  g_set_error (error,
               GTK_BUILDER_ERROR,
               GTK_BUILDER_ERROR_INVALID_VALUE,
               "Object named \"%s\" is of type \"%s\" which is not compatible "
               "with expected type \%s\"",
               string, G_OBJECT_TYPE_NAME (object), g_type_name (type));
}

Предупреждение PVS-Studio: V1094 The representation of conditional escape sequence '\%' is implementation-defined. Using this sequence in the string literal can lead to unexpected results. gtkbuilder.c 2674


В строковый литерал вкралась опечатка. Хотели экранировать двойные кавычки:


\"%s\"

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


\%s\"

Как будет обработана эта последовательность — зависит от компилятора.


Фрагмент N2. Опечатка в условии


Value* op_color_number(enum Sass_OP op, ....)
{
  double rval = rhs.value();

  if ((op == Sass_OP::DIV || op == Sass_OP::DIV) && rval == 0) {
    // comparison of Fixnum with Float failed?
    throw Exception::ZeroDivisionError(lhs, rhs);
  }
  ....
}

Предупреждение PVS-Studio: V501 [CWE-570] There are identical sub-expressions to the left and to the right of the '||' operator: op == Sass_OP::DIV || op == Sass_OP::DIV operators.cpp 250


Два раза переменная сравнивается с именованной константой Sass_OP::DIV. Есть большое подозрение, что второй константой должна быть Sass_OP::MOD.


Ошибку было бы проще заметить, если форматировать все однотипные блоки в условиях столбиком. Вот так:


if ((   op == Sass_OP::DIV
     || op == Sass_OP::DIV)
    && rval == 0) {
  // comparison of Fixnum with Float failed?
  throw Exception::ZeroDivisionError(lhs, rhs);
}

Когда написал, понял, что код смотрится неопрятно и стало ещё хуже, чем было. Хотя я большой сторонник табличного форматирования кода, в данном случае оно как-то неуместно получается. Если вам непонятен термин "табличное форматирование", то предлагаю познакомиться с ним в мини-книге "60 антипаттернов для С++ программиста". Смотрите двадцатый вредный совет "Компактный код".


Фрагмент N3. Бессмысленный вызов g_free


gtk_css_parser_resolve_url (GtkCssParser *self,
                            const char   *url)
{
  char *scheme;

  scheme = g_uri_parse_scheme (url);
  if (scheme != NULL)
    {
      GFile *file = g_file_new_for_uri (url);
      g_free (scheme);
      return file;
    }
  g_free (scheme);     // <=

  if (self->directory == NULL)
    return NULL;

  return g_file_resolve_relative_path (self->directory, url);
}

Предупреждение PVS-Studio: V575 [CWE-628, CERT-EXP37-C] The null pointer is passed into 'g_free' function. Inspect the first argument. gtkcssparser.c 189


Здесь второй вызов функции g_free явно лишний, так как в него всегда передаётся NULL. Ничего страшного из-за этого не случится. В g_free можно передать нулевой указатель. Просто лишня строчка кода.


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


Фрагмент N4. Переусложнённая проверка


static gboolean
is_codepoint (const char *str)
{
  int i;

  /* 'U' is not code point but 'U00C0' is code point */
  if (str[0] == '\0' || str[0] != 'U' || str[1] == '\0')
    return FALSE;

  for (i = 1; str[i] != '\0'; i++)
    {
      if (!g_ascii_isxdigit (str[i]))
        return FALSE;
    }

  return TRUE;
}

Предупреждение PVS-Studio: V590 [CWE-571, CERT-MSC01-C] Consider inspecting the 'str[0] == '\0' || str[0] != 'U'' expression. The expression is excessive or contains a misprint. gtkcomposetable.c 119


Условие:


if (str[0] == '\0' || str[0] != 'U' || str[1] == '\0')
  return FALSE;

Можно сократить:


if (str[0] != 'U' || str[1] == '\0')
  return FALSE;

С точки зрения логики работы кода ничего не изменилось. Так стоит ли упрощать код? Я считаю, что да. Чем сложнее код, тем больше он "притягивает" ошибки.


Фрагмент N5-N11. Потенциальное использование нулевого указателя


static void
notify_observers_added (GtkActionMuxer *muxer,
                        GtkActionMuxer *parent)
{
  ....
  Action *action;
  ....
  while (....)
  {
    ....
    if (!action->watchers)
      continue;

    for (node = action ? action->watchers : NULL; node; node = node->next)
      gtk_action_observer_primary_accel_changed (node->data,
                                                 GTK_ACTION_OBSERVABLE (muxer),
                                                 action_name, NULL);
  ....
}

Предупреждение PVS-Studio: V595 [CWE-476, CERT-EXP12-C] The 'action' pointer was utilized before it was verified against nullptr. Check lines: 449, 452. gtkactionmuxer.c 449


Указатель action используется, а затем проверяется на равенство NULL.


if (!action->watchers)
....
for (node = action ? ....)

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


if (action == NULL || action->watchers == NULL)
  continue;

for (node = action->watchers; node; node = node->next)
  gtk_action_observer_primary_accel_changed (node->data,
                                             GTK_ACTION_OBSERVABLE (muxer),
                                             action_name, NULL);

Это не единственная подобная ошибка в коде. Как минимум она присутствует в следующих местах:


  • V595 [CWE-476, CERT-EXP12-C] The 'icon' pointer was utilized before it was verified against nullptr. Check lines: 2225, 2231. gtkicontheme.c 2225
  • V595 [CWE-476, CERT-EXP12-C] The 'iw' pointer was utilized before it was verified against nullptr. Check lines: 194, 199. inspect-button.c 194
  • V595 [CWE-476, CERT-EXP12-C] The 'contents' pointer was utilized before it was verified against nullptr. Check lines: 493, 501. file.cpp 493
  • V595 [CWE-476, CERT-EXP12-C] The 'dispatch->data_poll' pointer was utilized before it was verified against nullptr. Check lines: 1519, 1523. gtkprintbackendcups.c 1519
  • V595 [CWE-476, CERT-EXP12-C] The 'top' pointer was utilized before it was verified against nullptr. Check lines: 1024, 1051. gdkscreen-x11.c 1024
  • V595 [CWE-476, CERT-EXP12-C] The 'dispatch->backend' pointer was utilized before it was verified against nullptr. Check lines: 1615, 1624. gtkprintbackendcups.c 1615

Фрагмент N12. Несовместимые способы выделения и освобождения памяти


static char *
strip_ulines (const char *text,
              guint      *accel_key)
{
  ....
  new_text = malloc (strlen (text) + 1);     // <=
  ....
  return new_text;
}

static void
finish_text (UriParserData *pdata)
{
  ....
  char *text;
  ....
  if (pdata->strip_ulines && strchr (pdata->text_data->str, '_'))
  {
    text = strip_ulines (pdata->text_data->str, &pdata->accel_key);  // <=
    text_len = strlen (text);
  }
  else
  {
    text = pdata->text_data->str;
    text_len = pdata->text_data->len;
  }
  ....
  if (text != pdata->text_data->str)
    g_free (text);                                                   // <=
  ....
}

Предупреждение PVS-Studio: V611 [CWE-762, CERT-MEM51-CPP] The memory was allocated using 'malloc' function but was released using the 'g_free' function. Consider inspecting operation logics behind the 'text' variable. gtklabel.c 3383


Память выделяется с помощью функции malloc, а освобождается с помощью функции g_free. В разделе "Memory Allocation" Glib указано, что так делать нельзя:


It's important to match g_malloc() (and wrappers such as g_new()) with g_free(), g_slice_alloc() (and wrappers such as g_slice_new()) with g_slice_free(), plain malloc() with free(), and (if you're using C++) new with delete and new[] with delete[]. Otherwise bad things can happen, since these allocators may use different memory pools (and new/delete call constructors and destructors).

Фрагмент N13. CWE-14


void overwrite_and_free (gpointer data)
{
  char *password = (char *) data;

  if (password != NULL)
    {
      memset (password, 0, strlen (password));
      g_free (password);
    }
}

Предупреждение PVS-Studio: V597 [CWE-14, CERT-MSC06-C] The compiler could delete the 'memset' function call, which is used to flush 'password' object. The memset_s() function should be used to erase the private data. gtkprintbackendcups.c 855


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


Пожалуй, надо будет отдельно исследовать этот момент. Интересно попробовать повторить, действительно ли получится скомпилировать код библиотеки так, чтобы вызов функции memset исчез. Решил попозже поэкспериментировать.


Ссылки для тех, что ещё не знаком с этим паттерном ошибки:



Фрагмент N14. Невалидный указатель


Offset Offset::init(const char* beg, const char* end)
{
  Offset offset(0, 0);
  if (end == 0) {
    end += strlen(beg);
  }
  offset.add(beg, end);
  return offset;
}

Предупреждение PVS-Studio: V769 [CWE-119, CERT-EXP08-C] The 'end' pointer in the 'end += strlen(beg)' expression equals nullptr. The resulting value is senseless and it should not be used. position.cpp 36


Если указатель end нулевой, то к нему прибавляется длина строки. В результате получается невалидный указатель, который нельзя использовать. Скорее всего, на самом деле хотели написать вот это:


end = beg + strlen(beg);

Фрагмент N15. Утечка памяти


static gboolean
query_tooltip (....)
{
  ....
  char *s, *s2;
  ....
  do {
    s = g_strdup_printf ("%.*f", precision, self->scale);
    l = strlen (s) - 1;
    while (s[l] == '0')
      l--;
    if (s[l] == '.')
      s[l] = '\0';
    precision++;
  } while (strcmp (s, "0") == 0);

  label = gtk_label_new (s);
  g_free (s);
  ....
}

Предупреждение PVS-Studio: V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The 's' pointer was assigned values twice without releasing the memory. A memory leak is possible. demo3widget.c 79


Функция g_strdup_printf схожа со стандартной C функцией sprintf, но безопасней, так как она рассчитывает максимальное требуемое пространство и выделяет нужное количество памяти, в которую записывает результирующую строку. Возвращаемая строка должна освобождаться, когда больше не нужна.


Вот как раз освобождения буфера в этом коде нет. Поэтому на каждой итерации цикла будет происходить утечка памяти. Следует добавить вызов g_free при каждой новой итерации цикла:


s = NULL;
do {
  g_free(s); // В g_free можно безопасно передавать NULL.
  s = g_strdup_printf ("%.*f", precision, self->scale);
  l = strlen (s) - 1;
  while (s[l] == '0')
    l--;
  if (s[l] == '.')
    s[l] = '\0';
  precision++;
} while (strcmp (s, "0") == 0);

label = gtk_label_new (s);
g_free (s);

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


Фрагмент N16-29. Более простые утечки памяти


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


static void
update_columns (GtkTreeView *view, ViewColumnModel *view_model)
{
  ....
  int *new_order;
  new_order = g_new (int, length);      // <=
  ....
  while (a->data == b->data)
  {
    a = a->next;
    b = b->next;
    if (a == NULL)
      return;                           // <=
    m++;
  }
  ....
  g_free (new_order);
  ....
}

Предупреждение PVS-Studio: V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'new_order' pointer. A memory leak is possible. testtreecolumns.c 441


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


Другие ошибки такого-же типа...
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'data' pointer. A memory leak is possible. widget-factory.c 1307
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'args' pointer. A memory leak is possible. gskglshader.c 1004
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'url' pointer. A memory leak is possible. gtkcsstokenizer.c 973
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'str' pointer. A memory leak is possible. gtkimcontextsimple.c 483
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'info' pointer. A memory leak is possible. gtkprintbackendcups.c 3034
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'resource' pointer. A memory leak is possible. gtkprintbackendcups.c 4165
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'string' pointer. A memory leak is possible. maplistmodel.c 62
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'string' pointer. A memory leak is possible. noselection.c 62
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'string' pointer. A memory leak is possible. slicelistmodel.c 61
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'string' pointer. A memory leak is possible. multiselection.c 62
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'string' pointer. A memory leak is possible. sortlistmodel.c 62
  • V773 [CWE-401, CERT-MEM31-C, CERT-MEM51-CPP] The function was exited without releasing the 'string' pointer. A memory leak is possible. singleselection.c 62

Фрагмент N30. Ошибка в логике


static void
gtk_section_model_default_get_section (GtkSectionModel *self,
                                       guint            position,
                                       guint           *out_start,
                                       guint           *out_end)
{
  guint n_items = g_list_model_get_n_items (G_LIST_MODEL (self));

  if (position >= n_items)
    {
      *out_start = n_items;
      *out_end = G_MAXUINT;
    }

  *out_start = 0;
  *out_end = n_items;
}

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


  • V519 [CWE-563, CERT-MSC13-C] The '* out_start' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 67, 71. gtksectionmodel.c 71
  • V519 [CWE-563, CERT-MSC13-C] The '* out_end' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 68, 72. gtksectionmodel.c 72

С кодом явно что-то не так. Нет смысла перезаписывать значения переменных. Возможно, в коде не хватает else-ветки:


if (position >= n_items)
{
  *out_start = n_items;
  *out_end = G_MAXUINT;
}
else
{
  *out_start = 0;
  *out_end = n_items;
}

Я не уверен, что код следует исправлять именно так, т.к. не знаком с устройством проекта GTK. Я могу только сказать, что код содержит аномалию, а вот как её корректно исправить – могу только предполагать.


Фрагмент N31. Красивая опечатка в условии


static int
gtk_tree_list_row_sort_keys_compare (gconstpointer a,
                                     gconstpointer b,
                                     gpointer      data)
{
  ....
  gpointer *keysa = (gpointer *) a;
  gpointer *keysb = (gpointer *) b;
  ....
  resa = unpack (keysa, &keysa, &sizea);
  resb = unpack (keysb, &keysb, &sizeb);
  if (!resa)
    return resb ? GTK_ORDERING_LARGER :
      (keysa[2] < keysb[2] ? GTK_ORDERING_SMALLER : 
      (keysb[2] > keysa[2] ? GTK_ORDERING_LARGER : GTK_ORDERING_EQUAL));
  else if (!resb)
    return GTK_ORDERING_SMALLER;
  ....
}

Попробуйте, не читая предупреждение, найти ошибку. Видите её? Наш единорог пока ждёт вас и пьёт кофе.


Единорог Коди пока ждёт вас и пьёт кофе


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


Предупреждение PVS-Studio: V547 [CWE-570] Expression 'keysb[2] > keysa[2]' is always false. gtktreelistrowsorter.c 158


Ошибка здесь:


.... keysa[2] < keysb[2] ? ....
.... keysb[2] > keysa[2] ? ....

Эти условия идентичны. Поменяли местами массивы и заменили < на >. В итоге получилось то же самое.


Фрагмент N32. Символ табуляции


И в конце очень красивый пример "кода с запахом". Прям с ног валит :)


Код с Tab


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


Видите эти "два пробела"? Так вот, это не пробелы, а символ табуляции!


Код с Tab


Вместо того, чтобы написать '\t', кто-то, не задумываясь, нажал символ табуляции. Очень плохая идея по множеству причин:


  • Код сбивает с толку;
  • Может так совпасть, что символ табуляции вообще будет выглядеть в коде как пробел, что ещё больше будет сбивать с толку;
  • При рефакторинге, автоформатировании кода, слиянии кода и так далее, символ табуляции легко может быть заменён на несколько пробелов.

Использование табуляции в строковых литералах я описал в книге "60 антипаттернов для С++ программиста". Приглашаю почитать, если хочется узнать ещё больше способов, как не надо писать код.


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


Фрагмент N33. Не та константа


typedef enum {
  GDK_MEMORY_B8G8R8A8_PREMULTIPLIED,
  GDK_MEMORY_A8R8G8B8_PREMULTIPLIED,
  GDK_MEMORY_R8G8B8A8_PREMULTIPLIED,
  ....
  // Тут много вырезано.
  // Всего здесь 27 именованных констант.
  // GDK_MEMORY_N_FORMATS - двадцать восьмая,
  // но равна она 27, так как значения идут от 0.
  ....
  GDK_MEMORY_A16 GDK_AVAILABLE_ENUMERATOR_IN_4_12,
  GDK_MEMORY_A16_FLOAT GDK_AVAILABLE_ENUMERATOR_IN_4_12,
  GDK_MEMORY_A32_FLOAT GDK_AVAILABLE_ENUMERATOR_IN_4_12,

  GDK_MEMORY_N_FORMATS
} GdkMemoryFormat;

static const char *format_name[] = {
  "BGRAp", "ARGBp", "RGBAp",
  "BGRA", "ARGB", "RGBA", "ABGR",
  "RGB", "BGR", NULL
};

static const char *
format_to_string (GdkMemoryFormat format)
{
  if (format < GDK_MEMORY_N_FORMATS)
    return format_name[format];
  else
    return "ERROR";
}

Предупреждение PVS-Studio: V557 [CWE-119, CERT-ARR30-C] Array overrun is possible. The value of 'format' index could reach 27. testupload.c 13


Константа GDK_MEMORY_N_FORMATS не имеет никакого отношения к массиву format_name. В массиве явно меньше элементов, чем 27. Так что проверка совсем не защищает от выхода за границу массива. Это интересный вариант опечатки, которая даже не понятно, как появилась. Мне не удалось найти рядом подходящих констант для проверки, поэтому на это место точно стоит взглянуть разработчикам.


Спасибо за внимание


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


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


Приглашаю попробовать PVS-Studio на ваших рабочих или домашних проектах.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Typos, null pointers, and treacherous TAB: 33 fragments in the GTK library.

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


  1. Revertis
    26.10.2023 08:22
    +6

    Всегда интересно почитать ваши разборы всякого опенсорса! Спасибо!


  1. mrobespierre
    26.10.2023 08:22
    +1

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


    1. Andrey2008 Автор
      26.10.2023 08:22

      Часть попала из subprojects.


  1. numb13
    26.10.2023 08:22
    +1

    Надеюсь, благодаря этой статье в библиотеке GTK будет исправлено несколько ошибок,

    https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gtk/gtkbuilder.c#L2674

    А в гноме о них знают?


    1. Andrey2008 Автор
      26.10.2023 08:22
      +1

      Да, но нет.

      Please, open merge requests or specific issues, instead of pointing to a blog post.

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


      1. numb13
        26.10.2023 08:22
        +1

        ИМХО нужно было портянку логов туда приложить.

        • V519 [CWE-563, CERT-MSC13-C] The '* out_start' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 67, 71. gtksectionmodel.c 71

        Мне тоже кажется что неправильно в багтрекере ссылаться только на внешний ресурс.