Wireshark 3.x: анализ кода под macOS и обзор ошибок
Wireshark Foundation выпустила финальную stable-версию популярного сетевого анализатора трафика — Wireshark 3.0.0. В новом релизе устранено несколько багов, реализована возможность анализа новых протоколов и заменен драйвер WinPcap на Npcap. Здесь заканчивается цитирование анонса и начинается наша заметка о багах в проекте. Перед релизом их было исправлено явно недостаточно. Давайте насобираем исправлений, чтобы был повод делать новый релиз :).
Введение
Wireshark — это достаточно известный инструмент для захвата и анализа сетевого трафика. Программа работает с подавляющим большинством известных протоколов, имеет понятный и логичный графический интерфейс, мощнейшую систему фильтров. Wireshark — кроссплатформенный, работает в таких ОС, как: Windows, Linux, macOS, Solaris, FreeBSD, NetBSD и многих других.
Для поиска ошибок использовался статический анализатор PVS-Studio. Для анализа исходного кода необходимо предварительно скомпилировать проект в какой-нибудь операционной системе. Выбор был большой не только из-за кроссплатформенности проекта, но и из-за кроссплатформенности анализатора. Для анализа проекта я выбрал macOS. Также запуск анализатора возможен в Windows и Linux.
Про качество кода хочется рассказать отдельно. К сожалению, я не могу назвать его хорошим. Это субъективная оценка, но поскольку мы регулярно проверяем множество проектов, у меня есть с чем сравнивать. В данном случае в глаза бросается большое количество предупреждений PVS-Studio на небольшом объёме кода. Суммарно на этот проект выдано более 3500 предупреждений всех уровней. Это характерно для проектов, в которых вообще не используются инструменты статического анализа, даже бесплатные. Другим фактором, указывающим на качество проекта, являются повторяющиеся ошибки, выявленные анализатором. В статье однотипные примеры кода не будут приводиться, но некоторые одинаковые ошибки присутствуют в коде в сотне мест.
Ещё качества коду не добавляют и такие вставки:
/* Input file: packet-acse-template.c */
#line 1 "./asn1/acse/packet-acse-template.c"
Их встречается более 1000 во всём проекте. Такие вставки усложняют анализатору сопоставление выдаваемых предупреждений с нужным файлом. Но я уверен, что и обычные разработчики не получают удовольствия от поддержки такого кода.
Опечатки
Предупреждение 1
V641 The size of the allocated memory buffer is not a multiple of the element size. mate_setup.c 100
extern mate_cfg_gog* new_gogcfg(mate_config* mc, gchar* name) {
mate_cfg_gog* cfg = (mate_cfg_gog *)g_malloc(sizeof(mate_cfg_gop));
....
}
Есть структуры двух типов: mate_cfg_gog и mate_cfg_gop, они очень похожи, но не идентичны. Скорее всего, в этом фрагменте кода функции перепутали, что чревато потенциальными ошибками в программе во время обращения к памяти по указателю.
Ниже приведены фрагменты перепутанных структур данных:
typedef struct _mate_cfg_gog {
gchar* name;
GHashTable* items;
guint last_id;
GPtrArray* transforms;
LoAL* keys;
AVPL* extra;
float expiration;
gop_tree_mode_t gop_tree_mode;
gboolean show_times;
....
} mate_cfg_gog;
typedef struct _mate_cfg_gop {
gchar* name;
guint last_id;
GHashTable* items;
GPtrArray* transforms;
gchar* on_pdu;
AVPL* key;
AVPL* start;
AVPL* stop;
AVPL* extra;
float expiration;
float idle_timeout;
float lifetime;
gboolean drop_unassigned;
gop_pdu_tree_t pdu_tree_mode;
gboolean show_times;
....
} mate_cfg_gop;
Предупреждение 2
V519 The 'HDR_TCP.dest_port' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 495, 496. text_import.c 496
void write_current_packet (void)
{
....
HDR_TCP.source_port =isOutbound ? g_htons(hdr_dest_port):g_htons(hdr_src_port);
HDR_TCP.dest_port = isOutbound ? g_htons(hdr_src_port) :g_htons(hdr_dest_port);
HDR_TCP.dest_port = g_htons(hdr_dest_port);
....
}
В последней строке безусловно перетирается только что вычисленное значение переменной HDR_TCP.dest_port.
Логические ошибки
В этом разделе приведу несколько примеров ошибок в условных операторах, причём все они будут принципиально отличаться друг от друга.
Предупреждение 1
V547 Expression 'direction == 0' is always false. packet-adb.c 291
#define P2P_DIR_RECV 1
#define P2P_DIR_SENT 0
static void
save_command(....)
{
....
if ( service_data
&& service_data->remote_id == 0
&& direction == P2P_DIR_RECV) {
if (direction == P2P_DIR_SENT) {
service_data->remote_id = arg1; // unreachable code
} else {
service_data->remote_id = arg0;
}
....
}
....
}
Во внешнем условии переменная direction сравнивается с константой P2P_DIR_RECV. Запись выражений через оператор AND означает, что при достижении внутреннего условия значение переменной direction точно не будет равно другой константе P2P_DIR_SENT.
Предупреждение 2
V590 Consider inspecting the '(type == 0×1) || (type!= 0×4)' expression. The expression is excessive or contains a misprint. packet-fcsb3.c 686
static int dissect_fc_sbccs (....)
{
....
else if ((type == FC_SBCCS_IU_CMD_HDR) ||
(type != FC_SBCCS_IU_CMD_DATA)) {
....
}
Ошибка этого фрагмента кода заключается в том, что результат условия зависит только от одного выражения:
(type != FC_SBCCS_IU_CMD_DATA)
Предупреждение 3
V590 Consider inspecting this expression. The expression is excessive or contains a misprint. snort-config.c 40
static char *skipWhiteSpace(char *source, int *accumulated_offset)
{
int offset = 0;
/* Skip any leading whitespace */
while (source[offset] != '\0' && source[offset] == ' ') {
offset++;
}
*accumulated_offset += offset;
return source + offset;
}
Результат условного оператора будет зависеть только от этой части выражения (source[offset] == ' '). Проверка (source[offset] != '\0') является избыточной и её можно смело удалить. Это не является настоящей ошибкой, но избыточный код затрудняет чтение и понимание программы, поэтому лучше его упростить.
Предупреждение 4
V547 Expression 'eras_pos!= NULL' is always true. reedsolomon.c 659
int
eras_dec_rs(dtype data[NN], int eras_pos[NN-KK], int no_eras)
{
....
if(eras_pos != NULL){
for(i=0;i
Возможно, мы имеем дело с лишней проверкой, а возможно — с опечаткой, и в одном из if-ов должно проверяться совсем иное.
Странные ассерты
Предупреждение 1
V547 Expression 'sub_dissectors!= NULL' is always true. capture_dissectors.c 129
void capture_dissector_add_uint(....)
{
....
sub_dissectors = (struct capture_dissector_table*)g_hash_table_lookup(....);
if (sub_dissectors == NULL) {
fprintf(stderr, "OOPS: Subdissector \"%s\" not found ... \n", name);
if (getenv("WIRESHARK_ABORT_ON_DISSECTOR_BUG") != NULL)
abort();
return;
}
g_assert(sub_dissectors != NULL); // <=
....
}
Проверка указателя в g_assert в этом месте является лишней, так как указатель проверяется перед этим. Возможно, в этой функции раньше был только g_assert, и его забыли удалить, но, возможно, тут следовало проверить какое-нибудь поле структуры.
Предупреждение 2
V547 Expression 'i < count' is always true. packet-netflow.c 10363
static int
dissect_v9_v10_template_fields(....)
{
....
count = tmplt_p->field_count[fields_type];
for(i=0; ifields_p[fields_type] != NULL) {
DISSECTOR_ASSERT (i < count); // <=
tmplt_p->fields_p[fields_type][i].type = type;
tmplt_p->fields_p[fields_type][i].length = length;
tmplt_p->fields_p[fields_type][i].pen = pen;
tmplt_p->fields_p[fields_type][i].pen_str = pen_str;
if (length != VARIABLE_LENGTH) {/
tmplt_p->length += length;
}
}
....
}
....
}
Не совсем понятно, зачем в функции присутствует assert, который дублирует условие из цикла. Счётчик цикла в теле не изменяется.
Ошибки с указателями
Предупреждение 1
V595 The 'si→conv' pointer was utilized before it was verified against nullptr. Check lines: 2135, 2144. packet-smb2.c 2135
static int
dissect_smb2_fid(....)
{
....
g_hash_table_insert(si->conv->fids, sfi, sfi); // <=
si->file = sfi;
if (si->saved) {
si->saved->file = sfi;
si->saved->policy_hnd = policy_hnd;
}
if (si->conv) { // <=
eo_file_info = (.... *)g_hash_table_lookup(si->conv->files,&policy_hnd);
....
}
....
}
Указатель si→conv разыменовывается на несколько строк ранее, чем проверяется, равен он нулю или нет.
Предупреждение 2
V774 The 'protos' pointer was used after the memory was released. packet-k12.c 311
static gboolean
k12_update_cb(void* r, char** err)
{
gchar** protos;
....
for (i = 0; i < num_protos; i++) {
if ( ! (h->handles[i] = find_dissector(protos[i])) ) {
h->handles[i] = data_handle;
h->handles[i+1] = NULL;
g_strfreev(protos);
*err = g_strdup_printf("Could not find dissector for: '%s'", protos[i]);
return FALSE;
}
}
....
}
protos — это массив строк. Во время обработки особой ситуации в программе, этот массив сначала очищается функцией g_strfreev, а потом в сообщении о ошибке используется одна из строк этого массива. Скорее всего, эти строки в коде следует поменять местами:
*err = g_strdup_printf("Could not find dissector for: '%s'", protos[i]);
g_strfreev(protos);
Утечки памяти
V773 The 'ptmpstr' pointer was assigned values twice without releasing the memory. A memory leak is possible. idl2wrs.c 2436
static void parsetypedefunion(int pass)
{
char tmpstr[BASE_BUFFER_SIZE], *ptmpstr;
....
while(num_pointers--){
g_snprintf(tmpstr, BASE_BUFFER_SIZE, "%s_%s", ptmpstr, "unique");
FPRINTF(eth_code, "static int\n");
FPRINTF(eth_code, "....", tmpstr);
FPRINTF(eth_code, "{\n");
FPRINTF(eth_code, " ....", ptmpstr, ti->str);
FPRINTF(eth_code, " return offset;\n");
FPRINTF(eth_code, "}\n");
FPRINTF(eth_code, "\n");
ptmpstr=g_strdup(tmpstr);
}
....
}
После функции g_strdup необходимо в какой-то момент вызывать функцию g_free. В представленном фрагменте кода это не делается, и в цикле на каждой итерации выделяется новый участок оперативной памяти. Возникают множественные утечки памяти.
Ещё несколько предупреждений на похожие фрагменты кода:
- V773 The 'ptmpstr' pointer was assigned values twice without releasing the memory. A memory leak is possible. idl2wrs.c 2447
- V773 The 'ptmpstr' pointer was assigned values twice without releasing the memory. A memory leak is possible. idl2wrs.c 2713
- V773 The 'ptmpstr' pointer was assigned values twice without releasing the memory. A memory leak is possible. idl2wrs.c 2728
- V773 The 'ptmpstr' pointer was assigned values twice without releasing the memory. A memory leak is possible. idl2wrs.c 2732
- V773 The 'ptmpstr' pointer was assigned values twice without releasing the memory. A memory leak is possible. idl2wrs.c 2745
К сожалению, в коде ещё много других подобных мест, где не освобождается память.
Разное
Предупреждение 1
V535 The variable 'i' is being used for this loop and for the outer loop. Check lines: 7716, 7798. packet-opa-mad.c 7798
/* Parse GetVFInfo MAD from the Performance Admin class. */
static gint parse_GetVFInfo(....)
{
....
for (i = 0; i < records; i++) { // <= line 7716
....
for (i = 0; i < PM_UTIL_BUCKETS; i++) { // <= line 7748
GetVFInfo_Util_Stats_Bucket_item = proto_tree_add_item(....);
proto_item_set_text(....);
local_offset += 4;
}
....
for (i = 0; i < PM_ERR_BUCKETS; i++) { // <= line 7798
GetVFInfo_Error_Stats_Bucket_item = proto_tree_add_item(....);
proto_item_set_text(....);
local_offset += 4;
....
}
....
}
....
}
В очень длинной функции разработчики смело изменяют значение счётчика цикла, причём делают это несколько раз. Сложно сказать, является это ошибкой или нет, но подобных циклов в проекте около 10.
Предупреждение 2
V763 Parameter 'item' is always rewritten in function body before being used. packet-cdma2k.c 1324
static void cdma2k_message_ORDER_IND(proto_item *item, ....)
{
guint16 addRecLen = -1, ordq = -1, rejectedtype = -1;
guint16 l_offset = -1, rsc_mode_ind = -1, ordertype = -1;
proto_tree *subtree = NULL, *subtree1 = NULL;
item = proto_tree_add_item(tree,hf_cdma2k_OrderIndMsg, tvb, ....); // <=
subtree = proto_item_add_subtree(item, ett_cdma2k_subtree1);
....
}
Указатель item, который принимает функция, сразу перетирается другим значением. Это очень подозрительно. Причём в коде присутствует несколько десятков таких мест, поэтому сложно сказать, ошибка это или нет. Похожий код я ранее встречал в другом большом проекте, там это был верный код, просто никто не решался изменить интерфейс функции.
Предупреждение 3
V762 It is possible a virtual function was overridden incorrectly. See third argument of function 'headerData' in derived class 'PacketListModel' and base class 'QAbstractItemModel'. packet_list_model.h 48
QVariant
QAbstractItemModel::headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const // <=
class PacketListModel : public QAbstractItemModel
{
Q_OBJECT
public:
....
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole | Qt::ToolTipRole) const; // <=
....
};
Анализатор обнаружил некорректную перегрузку функции headerData. У функций отличается дефолтное значение параметра role. Это может приводить не к тому поведению, которое ожидал программист.
Предупреждение 4
V610 Undefined behavior. Check the shift operator '>>'. The right operand ('bitshift' = [0…64]) is greater than or equal to the length in bits of the promoted left operand. proto.c 10941
static gboolean
proto_item_add_bitmask_tree(...., const int len, ....)
{
....
if (len < 0 || len > 8)
g_assert_not_reached();
bitshift = (8 - (guint)len)*8;
available_bits = G_GUINT64_CONSTANT(0xFFFFFFFFFFFFFFFF) >> bitshift;
....
}
Сдвиг числа на 64 бита приведёт к неопределённому поведению согласно стандарту языка.
Скорее, правильный код должен быть таким:
if (bitshift == 64)
available_bits = 0;
else
available_bits = G_GUINT64_CONSTANT(0xFFFFFFFFFFFFFFFF) >> bitshift;
Заключение
Может показаться, что в обзоре приведено мало примеров ошибок, но в полном отчёте представленные случаи повторяются десятки и сотни раз. Обзоры предупреждений PVS-Studio носят демонстрационный характер. Это вклад в качество проектов с открытым исходным кодом, но разовые проверки — самый неэффективный способ применения методологии статического анализа.
Вы можете получить и проанализировать полный отчет самостоятельно. Для этого просто необходимо скачать и запустить анализатор PVS-Studio.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Svyatoslav Razmyslov. Wireshark 3.x: code analysis under macOS and errors review