Checking FreeRDP with PVS-Studio

WILD

Administrator
Staff member
ADMIN
SELLER
SUPREME
MEMBER
Joined
Jan 21, 2025
Messages
220
Reaction score
631
Deposit
0$
FreeRDP — это реализация протокола удаленного рабочего стола (RDP) с открытым исходным кодом, проприетарного протокола Microsoft. Проект поддерживает множество платформ, включая Windows, Linux, macOS и даже iOS и Android. Мы выбрали его в качестве первого проекта, проанализированного с помощью статического анализатора кода PVS-Studio для серии статей о проверках RDP-клиентов.

Немного истории​


Проект FreeRDP был запущен после того, как Microsoft открыла спецификации своего проприетарного протокола RDP. На тот момент уже использовался клиент rdesktop, созданный в основном на основе работ по обратной разработке.

В процессе разработки протокола разработчики столкнулись с трудностями при добавлении новой функциональности из-за архитектурных проблем. Изменения в архитектуре вызвали конфликт между разработчиками и привели к созданию форка rdesktop, известного как FreeRDP. Дальнейшее распространение было ограничено лицензией GPLv2, и авторы решили перейти на лицензию Apache License v2. Однако некоторые не захотели менять лицензию, поэтому разработчики решили переписать кодовую базу с нуля, и так появился проект в том виде, в котором мы его знаем сегодня.

Полная история проекта доступна в официальном блоге: « История проекта FreeRDP ».

Для сканирования проекта на наличие ошибок и потенциальных уязвимостей я использовал PVS-Studio . PVS-Studio — это статический анализатор кода, написанного на C, C++, C# и Java, и он работает на Windows, Linux и macOS.

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


Утечка памяти​


V773 Функция завершилась без освобождения указателя 'cwd'. Возможна утечка памяти. environment.c 84

DWORD GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer)
{
char* cwd;
....
cwd = getcwd(NULL, 0);
....
если (lpBuffer == NULL)
{
free(cwd);
вернуть 0;
}

если ((длина + 1) > nBufferLength)
{
free(cwd);
return (DWORD) (length + 1);
}

memcpy(lpBuffer, cwd, length + 1);
длина возвращаемого значения;
....
}



Этот фрагмент кода взят из подсистемы winpr, которая реализует обертку WINAPI для систем, отличных от Windows, то есть выступает в качестве более легкого аналога Wine. Приведенный выше код содержит утечку памяти: память, выделенная функцией getcwd , освобождается только в особых случаях. Чтобы исправить это, авторам следует добавить вызов функции free после вызова memcpy .


Индекс массива выходит за пределы допустимого диапазона.​


V557 Возможно переполнение массива. Значение индекса 'event->EventHandlerCount' может достичь 32. PubSub.c 117

#define MAX_EVENT_HANDLERS 32

структура _wEventType
{
....
int EventHandlerCount;
pEventHandler EventHandlers[MAX_EVENT_HANDLERS];
};

int PubSub_Subscribe(wPubSub* pubSub, const char* EventName,
pEventHandler EventHandler)
{
....
if (event->EventHandlerCount <= MAX_EVENT_HANDLERS)
{
event->EventHandlers[event->EventHandlerCount] = EventHandler;
event->EventHandlerCount++;
}
....
}



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

Анализатор обнаружил еще одну ошибку этого типа:


  • V557 Возможно переполнение массива. Значение индекса 'iBitmapFormat' может достигать 8. orders.c 2623


Опечатки​



Фрагмент 1​


Выражение V547 '!pipe->In' всегда ложно. MessagePipe.c 63

wMessagePipe* MessagePipe_New()
{
....
pipe->In = MessageQueue_New(NULL);
if (!pipe->In)
перейти к error_in;

pipe->Out = MessageQueue_New(NULL);
if (!pipe->In) // <=
перейти к error_out;
....



}

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


Фрагмент 2​


V760 Были обнаружены два идентичных блока текста. Второй блок начинается со строки 771. tsg.c 770

typedef struct _TSG_PACKET_VERSIONCAPS
{
....
UINT16 majorVersion;
UINT16 minorVersion;
....
} TSG_PACKET_VERSIONCAPS, *PTSG_PACKET_VERSIONCAPS;

static BOOL TsProxyCreateTunnelReadResponse(....)
{
....
PTSG_PACKET_VERSIONCAPS versionCaps = NULL;
....
/* Основная версия (2 байта) */
Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
/* Дополнительная версия (2 байта) */
Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
....
}



Ещё одна опечатка: в комментарии говорится, что переменная minorVersion должна считываться из потока, а её значение считывается в переменную majorVersion . Однако я недостаточно хорошо знаком с проектом, чтобы утверждать это наверняка.


Фрагмент 3​


V524 Странно, что тело функции 'trio_index_last' полностью эквивалентно телу функции 'trio_index'. triostr.c 933

/**
Найти первое вхождение символа в строке.
....
*/
TRIO_PUBLIC_STRING char *
trio_index
TRIO_ARGS2((string, character),
TRIO_CONST char *string,
целочисленный символ)
{
assert(string);
return strchr(string, character);
}

/**
Найти последнее вхождение символа в строке.
....
*/
TRIO_PUBLIC_STRING char *
trio_index_last
TRIO_ARGS2((string, character),
TRIO_CONST char *string,
целочисленный символ)
{
assert(string);
return strchr(string, character);
}



Как следует из комментария, функция trio_index находит первое вхождение символа в строке, а функция trio_index_last — последнее. Однако тела обеих этих функций абсолютно одинаковы! Это, должно быть, опечатка, и функция trio_index_last , вероятно, должна возвращать strrchr вместо strchr — в этом случае программа будет работать как ожидалось.


Фрагмент 4​


V769 Указатель 'data' в выражении равен nullptr. Результирующее значение арифметических операций над этим указателем бессмысленно и не должно использоваться. nsc_encode.c 124

static BOOL nsc_encode_argb_to_aycocg(NSC_CONTEXT* context,
const BYTE* data,
UINT32 строка развертки)
{
....
если (!context || data || (scanline == 0))
вернуть FALSE;
....
src = data + (context->height - 1 - y) * scanline;
....
}



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


Фрагмент 5​


V517 Обнаружено использование шаблона 'if (A) {...} else if (A) {...}'. Существует вероятность наличия логической ошибки. Проверьте строки: 213, 222. rdpei_common.c 213

BOOL rdpei_write_4byte_unsigned(wStream* s, UINT32 value)
{
БАЙТ байт;

если (значение <= 0x3F)
{
....
}
иначе, если (значение <= 0x3FFF)
{
....
}
иначе, если (значение <= 0x3FFFFF)
{
байт = (значение >> 16) & 0x3F;
Stream_Write_UINT8(s, byte | 0x80);
байт = (значение >> 8) & 0xFF;
Stream_Write_UINT8(s, byte);
байт = (значение & 0xFF);
Stream_Write_UINT8(s, byte);
}
иначе, если (значение <= 0x3FFFFF)
{
байт = (значение >> 24) & 0x3F;
Stream_Write_UINT8(s, byte | 0xC0);
байт = (значение >> 16) & 0xFF;
Stream_Write_UINT8(s, byte);
байт = (значение >> 8) & 0xFF;
Stream_Write_UINT8(s, byte);
байт = (значение & 0xFF);
Stream_Write_UINT8(s, byte);
}
....
}



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

Ещё одна ошибка такого типа:


  • V517 Обнаружено использование шаблона 'if (A) {...} else if (A) {...}'. Существует вероятность наличия логической ошибки. Проверьте строки: 169, 173. file.c 169


Проверка входных данных​



Фрагмент 1​


V547 Выражение 'strcat(target, source) != NULL' всегда истинно. triostr.c 425

TRIO_PUBLIC_STRING int
trio_append
TRIO_ARGS2((target, source),
char *target,
TRIO_CONST char *source)
{
assert(target);
assert(source);

return (strcat(target, source) != NULL);
}



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


Фрагмент 2​


V547 Выражение 'cache' всегда истинно. glyph.c 730

typedef struct rdp_glyph_cache rdpGlyphCache;

структура rdp_glyph_cache
{
....
GLYPH_CACHE glyphCache[10];
....
};

void glyph_cache_free(rdpGlyphCache* glyphCache)
{
....
GLYPH_CACHE* cache = glyphCache->glyphCache;

если (кэш)
{
....
}
....
}



В этом фрагменте кода переменной cache присваивается адрес статического массива glyphCache->glyphCache . Поэтому проверку if (cache) можно удалить.


Ошибка управления ресурсами​


V1005 Ресурс был получен с помощью функции 'CreateFileA', но освобожден с помощью несовместимой функции 'fclose'. certificate.c 447

BOOL certificate_data_replace(rdpCertificateStore* certificate_store,
rdpCertificateData* certificate_data)
{
дескриптор fp;
....
fp = CreateFileA(certificate_store->file, GENERIC_READ | GENERIC_WRITE, 0,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
....
если (размер < 1)
{
CloseHandle(fp);
вернуть FALSE;
}
....
если (!данные)
{
fclose(fp);
вернуть FALSE;
}
....
}



Дескриптор файла, созданный функцией CreateFile , был по ошибке закрыт из-за вызова функции fclose из стандартной библиотеки вместо функции CloseHandle .


Идентичные условия​


V581 Условные выражения операторов 'if', расположенных рядом друг с другом, идентичны. Проверьте строки: 269, 283. ndr_structure.c 283

void NdrComplexStructBufferSize(PMIDL_STUB_MESSAGE pStubMsg,
unsigned char* pMemory, PFORMAT_STRING pFormat)
{
....
если (conformant_array_description)
{
Размер ULONG;
unsigned char array_type;
array_type = conformant_array_description[0];
размер = NdrComplexStructMemberSize(pStubMsg, pFormat);
WLog_ERR(TAG, "warning: NdrComplexStructBufferSize array_type: "
"0x%02X не реализовано", array_type);
NdrpComputeConformance(pStubMsg, pMemory + size,
conformant_array_description);
NdrpComputeVariance(pStubMsg, pMemory + size,
conformant_array_description);
MaxCount = pStubMsg->MaxCount;
ActualCount = pStubMsg->ActualCount;
Смещение = pStubMsg->Смещение;
}

если (conformant_array_description)
{
unsigned char array_type;
array_type = conformant_array_description[0];
pStubMsg->MaxCount = MaxCount;
pStubMsg->ActualCount = ActualCount;
pStubMsg->Offset = Offset;
WLog_ERR(TAG, "warning: NdrComplexStructBufferSize array_type: "
"0x%02X не реализовано", array_type);
}
....
}



Этот фрагмент может быть правильным, но подозрительно, что оба условия содержат идентичные сообщения — одно из них, вероятно, излишне.


Освобождение нулевых указателей​


V575 Нулевой указатель передается в функцию 'free'. Проверьте первый аргумент. smartcard_pcsc.c 875

WINSCARDAPI LONG WINAPI PCSC_SCardListReadersW(
SCARDCONTEXT hContext,
LPCWSTR mszGroups,
LPWSTR mszReaders,
LPDWORD pcchReaders)
{
LPSTR mszGroupsA = NULL;
....
mszGroups = NULL; /* mszGroups не поддерживается pcsc-lite */

если (mszGroups)
ConvertFromUnicode(CP_UTF8,0, mszGroups, -1,
(char**) &mszGroupsA, 0,
NULL, NULL);

статус = PCSC_SCardListReaders_Internal(hContext, mszGroupsA,
(LPSTR) &mszReadersA,
pcchReaders);

если (status == SCARD_S_SUCCESS)
{
....
}

free(mszGroupsA);
....
}



Функция free может быть вызвана для нулевого указателя, и PVS-Studio это знает. Но если указатель всегда оказывается нулевым, как в этом фрагменте кода, анализатор выдаст предупреждение.

Указатель mszGroupsA изначально установлен в NULL и нигде больше не инициализируется. Единственная ветвь, где его можно было бы инициализировать, недоступна.

Ещё несколько подобных предупреждений:


  • V575 Нулевой указатель передается в функцию 'free'. Проверьте первый аргумент. license.c 790
  • V575 Нулевой указатель передается в функцию 'free'. Проверьте первый аргумент. rdpsnd_alsa.c 575

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


Потенциальное переполнение​


V1028 Возможно переполнение. Рассмотрите возможность приведения типов операндов, а не результата. makecert.c 1087

// openssl/x509.h
ASN1_TIME *X509_gmtime_adj(ASN1_TIME *s, long adj);

структура _MAKECERT_CONTEXT
{
....
int duration_years;
int duration_months;
};

typedef struct _MAKECERT_CONTEXT MAKECERT_CONTEXT;

int makecert_context_process(MAKECERT_CONTEXT* context, ....)
{
....
если (context->duration_months)
X509_gmtime_adj(after, (long)(60 * 60 * 24 * 31 *
context->duration_months));
иначе, если (контекст->продолжительность_лет)
X509_gmtime_adj(after, (long)(60 * 60 * 24 * 365 *
context->duration_years));
....
}



Преобразование результата выражения в тип long не предотвратит переполнение, поскольку вычисление выполняется над значением, пока оно еще имеет тип int .


Разыменование указателя при инициализации​


V595 Указатель 'context' был использован до проверки на соответствие nullptr. Проверьте строки: 746, 748. gfx.c 746

static UINT gdi_SurfaceCommand(RdpgfxClientContext* context,
const RDPGFX_SURFACE_COMMAND* cmd)
{
....
rdpGdi* gdi = (rdpGdi*) context->custom;

если (!контекст || !см.)
return ERROR_INVALID_PARAMETER;
....
}



Указатель контекста разыменовывается во время его инициализации, то есть до проверки.

Другие ошибки этого типа:


  • V595 Указатель 'ntlm' был использован до того, как он был проверен на соответствие nullptr. Проверьте строки: 236, 255. ntlm.c 236
  • V595 Указатель 'context' был использован до проверки на соответствие nullptr. Проверьте строки: 1003, 1007. rfx.c 1003
  • V595 Указатель 'rdpei' был использован до того, как он был проверен на соответствие nullptr. Проверьте строки: 176, 180. rdpei_main.c 176
  • V595 Указатель 'gdi' был использован до проверки на соответствие nullptr. Проверьте строки: 121, 123. xf_gfx.c 121


Бессмысленное состояние​


V547 Выражение 'rdp->state >= CONNECTION_STATE_ACTIVE' всегда истинно. connection.c 1489

int rdp_server_transition_to_state(rdpRdp* rdp, int state)
{
....
переключатель (состояние)
{
....
case CONNECTION_STATE_ACTIVE:
rdp->state = CONNECTION_STATE_ACTIVE; // <=
....
if (rdp->state >= CONNECTION_STATE_ACTIVE) // <=
{
IFCALLRET(client->Activate, client->activated, client);

если (!клиент->активирован)
вернуть -1;
}
....
}
....
}


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


Некорректная обработка строк​


V576 Неправильный формат. Рекомендуется проверить третий фактический аргумент функции 'sscanf'. Ожидается указатель на тип unsigned int. proxy.c 220

V560 Часть условного выражения всегда истинна: (rc >= 0). proxy.c 222

static BOOL check_no_proxy(....)
{
....
int sub;
int rc = sscanf(range, "%u", &sub);

если ((rc == 1) && (rc >= 0))
{
....
}
....
}



Этот код вызывает сразу два предупреждения. Заполнитель %u используется для переменных типа unsigned int , в то время как подпеременная имеет тип int . Второе предупреждение указывает на подозрительную проверку: правая часть условного выражения не имеет смысла, поскольку переменная уже была проверена на 1 в левой части. Я не уверен в намерениях автора, но с этим кодом явно что-то не так.


Чеки проставлены в неправильном порядке.​


V547 Выражение 'status == 0x00090314' всегда ложно. ntlm.c 299

BOOL ntlm_authenticate(rdpNtlm* ntlm, BOOL* pbContinueNeeded)
{
....
если (статус != SEC_E_OK)
{
....
вернуть FALSE;
}

if (status == SEC_I_COMPLETE_NEEDED) // <=
статус = SEC_E_OK;
else if (status == SEC_I_COMPLETE_AND_CONTINUE) // <=
статус = SEC_I_CONTINUE_NEEDED;
....
}



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

если (status == SEC_I_COMPLETE_NEEDED)
статус = SEC_E_OK;
else if (status == SEC_I_COMPLETE_AND_CONTINUE)
статус = SEC_I_CONTINUE_NEEDED;
иначе, если (статус != SEC_E_OK)
{
....
вернуть FALSE;
}




Заключение​


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