ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° FreeRDP с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Π°Π½Π°Π»ΠΈΠ·Π°Ρ‚ΠΎΡ€Π° PVS-Studio

ο»ΏΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° FreeRDP с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Π°Π½Π°Π»ΠΈΠ·Π°Ρ‚ΠΎΡ€Π° PVS-Studio
FreeRDP – открытая рСализация Remote Desktop Protocol (RDP), ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Π°, Ρ€Π΅Π°Π»ΠΈΠ·ΡƒΡŽΡ‰Π΅Π³ΠΎ ΡƒΠ΄Π°Π»Π΅Π½Π½ΠΎΠ΅ ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠΎΠΌΠΏΡŒΡŽΡ‚Π΅Ρ€ΠΎΠΌ, Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Π°Π½Π½ΠΎΠ³ΠΎ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠ΅ΠΉ Microsoft. ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ мноТСство ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌ, срСди ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… Windows, Linux, macOS ΠΈ Π΄Π°ΠΆΠ΅ iOS с Android. Π­Ρ‚ΠΎΡ‚ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ Π²Ρ‹Π±Ρ€Π°Π½ ΠΏΠ΅Ρ€Π²Ρ‹ΠΌ Π² Ρ€Π°ΠΌΠΊΠ°Ρ… Ρ†ΠΈΠΊΠ»Π° статСй, посвящСнных ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ΅ RDP-ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΠΎΠ² с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ статичСского Π°Π½Π°Π»ΠΈΠ·Π°Ρ‚ΠΎΡ€Π° PVS-Studio.

НСмного истории

ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ FreeRDP появился послС Ρ‚ΠΎΠ³ΠΎ, ΠΊΠ°ΠΊ Microsoft ΠΎΡ‚ΠΊΡ€Ρ‹Π»Π° спСцификации своСго ΠΏΡ€ΠΎΠΏΡ€ΠΈΠ΅Ρ‚Π°Ρ€Π½ΠΎΠ³ΠΎ ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Π° RDP. На Ρ‚ΠΎΡ‚ ΠΌΠΎΠΌΠ΅Π½Ρ‚ сущСствовал ΠΊΠ»ΠΈΠ΅Π½Ρ‚ rdesktop, рСализация ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ базируСтся Π½Π° Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°Ρ… Reverse Engineering.

Π’ процСссС Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Π° ΡΡ‚Π°Π½ΠΎΠ²ΠΈΠ»ΠΎΡΡŒ слоТнСС Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ Π½ΠΎΠ²Ρ‹ΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π» ΠΈΠ·-Π·Π° ΡΡƒΡ‰Π΅ΡΡ‚Π²ΠΎΠ²Π°Π²ΡˆΠ΅ΠΉ Ρ‚ΠΎΠ³Π΄Π° Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°. ИзмСнСния Π² Π½Π΅ΠΉ ΠΏΠΎΡ€ΠΎΠ΄ΠΈΠ»ΠΈ ΠΊΠΎΠ½Ρ„Π»ΠΈΠΊΡ‚ ΠΌΠ΅ΠΆΠ΄Ρƒ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ°ΠΌΠΈ, Ρ‡Ρ‚ΠΎ ΠΏΡ€ΠΈΠ²Π΅Π»ΠΎ ΠΊ созданию Ρ„ΠΎΡ€ΠΊΠ° rdesktop β€” FreeRDP. Π”Π°Π»ΡŒΠ½Π΅ΠΉΡˆΠ΅Π΅ распространСниС ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚Π° Π±Ρ‹Π»ΠΎ ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΎ Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ GPLv2, Π² Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π΅ Ρ‡Π΅Π³ΠΎ Π±Ρ‹Π»ΠΎ принято Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅ ΠΎ Ρ€Π΅Π»ΠΈΡ†Π΅Π½Π·ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ Π½Π° Apache License v2. Однако Π½Π΅ всС Π±Ρ‹Π»ΠΈ согласны ΠΌΠ΅Π½ΡΡ‚ΡŒ Π»ΠΈΡ†Π΅Π½Π·ΠΈΡŽ своСго ΠΊΠΎΠ΄Π°, поэтому Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ Ρ€Π΅ΡˆΠΈΠ»ΠΈ ΠΏΠ΅Ρ€Π΅ΠΏΠΈΡΠ°Ρ‚ΡŒ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚, Π² Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π΅ Ρ‡Π΅Π³ΠΎ ΠΌΡ‹ ΠΈΠΌΠ΅Π΅ΠΌ соврСмСнный Π²ΠΈΠ΄ ΠΊΠΎΠ΄ΠΎΠ²ΠΎΠΉ Π±Π°Π·Ρ‹.

Π‘ΠΎΠ»Π΅Π΅ ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎ ΠΎΠ± истории ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡ€ΠΎΡ‡Π΅ΡΡ‚ΡŒ Π² Π·Π°ΠΌΠ΅Ρ‚ΠΊΠ΅ ΠΎΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ Π±Π»ΠΎΠ³Π°: Β«The history of the FreeRDP projectΒ».

Π’ качСствС инструмСнта для выявлСния ошибок ΠΈ ΠΏΠΎΡ‚Π΅Π½Ρ†ΠΈΠ°Π»ΡŒΠ½Ρ‹Ρ… уязвимостСй Π² ΠΊΠΎΠ΄Π΅ использовался PVS-Studio. Π­Ρ‚ΠΎ статичСский Π°Π½Π°Π»ΠΈΠ·Π°Ρ‚ΠΎΡ€ ΠΊΠΎΠ΄Π° для языков C, C++, C# ΠΈ Java, доступный Π½Π° ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ°Ρ… Windows, Linux ΠΈ macOS.

Π’ ΡΡ‚Π°Ρ‚ΡŒΠ΅ прСдставлСны лишь Ρ‚Π΅ ошибки, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ показались ΠΌΠ½Π΅ Π½Π°ΠΈΠ±ΠΎΠ»Π΅Π΅ интСрСсными.

Π£Ρ‚Π΅Ρ‡ΠΊΠ° памяти

V773 The function was exited without releasing the ‘cwd’ pointer. A memory leak is possible. environment.c 84

DWORD GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer)
{
  char* cwd;
  ....
  cwd = getcwd(NULL, 0);
  ....
  if (lpBuffer == NULL)
  {
    free(cwd);
    return 0;
  }

  if ((length + 1) > nBufferLength)
  {
    free(cwd);
    return (DWORD) (length + 1);
  }

  memcpy(lpBuffer, cwd, length + 1);
  return length;
  ....
}

Π”Π°Π½Π½Ρ‹ΠΉ Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ Π±Ρ‹Π» взят ΠΈΠ· подсистСмы winpr, Ρ€Π΅Π°Π»ΠΈΠ·ΡƒΡŽΡ‰Π΅ΠΉ ΠΎΠ±Π΅Ρ€Ρ‚ΠΊΡƒ WINAPI для Π½Π΅-Windows систСм, Ρ‚.Π΅. это Π»Π΅Π³ΠΊΠΈΠΉ Π°Π½Π°Π»ΠΎΠ³ Wine. Π—Π΄Π΅ΡΡŒ ΠΌΠΎΠΆΠ½ΠΎ Π·Π°ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ ΡƒΡ‚Π΅Ρ‡ΠΊΡƒ: ΠΏΠ°ΠΌΡΡ‚ΡŒ, выдСлСнная Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠ΅ΠΉ getcwd, освобоТдаСтся Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΡ€ΠΈ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅ ΡΠΏΠ΅Ρ†ΠΈΠ°Π»ΡŒΠ½Ρ‹Ρ… случаСв. Для устранСния ошибки Π½ΡƒΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π²Ρ‹Π·ΠΎΠ² free послС memcpy.

Π’Ρ‹Ρ…ΠΎΠ΄ Π·Π° Π³Ρ€Π°Π½ΠΈΡ†Ρ‹ массива

V557 Array overrun is possible. The value of ‘event->EventHandlerCount’ index could reach 32. PubSub.c 117

#define MAX_EVENT_HANDLERS  32

struct _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 Array overrun is possible. The value of ‘iBitmapFormat’ index could reach 8. orders.c 2623

ΠžΠΏΠ΅Ρ‡Π°Ρ‚ΠΊΠΈ

Π€Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ 1

V547 Expression ‘!pipe->In’ is always false. MessagePipe.c 63

wMessagePipe* MessagePipe_New()
{
  ....
  pipe->In = MessageQueue_New(NULL);
  if (!pipe->In)
    goto error_in;

  pipe->Out = MessageQueue_New(NULL);
  if (!pipe->In) // <=
    goto error_out;
  ....
}

Π’ΡƒΡ‚ ΠΌΡ‹ Π²ΠΈΠ΄ΠΈΠΌ ΠΎΠ±Ρ‹Ρ‡Π½ΡƒΡŽ ΠΎΠΏΠ΅Ρ‡Π°Ρ‚ΠΊΡƒ: Π²ΠΎ Π²Ρ‚ΠΎΡ€ΠΎΠΌ условии выполняСтся ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚ΠΎΠΉ ΠΆΠ΅ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ, Ρ‡Ρ‚ΠΎ ΠΈ Π² ΠΏΠ΅Ρ€Π²ΠΎΠΌ. Π‘ΠΊΠΎΡ€Π΅Π΅ всСго, ошибка появилась Π² Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π΅ Π½Π΅ΡƒΠ΄Π°Ρ‡Π½ΠΎΠ³ΠΎ копирования ΠΊΠΎΠ΄Π°.

Π€Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ 2

V760 Two identical blocks of text were found. The second block begins from line 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;
  ....
  /* MajorVersion (2 bytes) */
  Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
  /* MinorVersion (2 bytes) */
  Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
  ....
}

Π•Ρ‰Π΅ ΠΎΠ΄Π½Π° ΠΎΠΏΠ΅Ρ‡Π°Ρ‚ΠΊΠ°: ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ ΠΊ ΠΊΠΎΠ΄Ρƒ ΠΏΠΎΠ΄Ρ€Π°Π·ΡƒΠΌΠ΅Π²Π°Π΅Ρ‚, Ρ‡Ρ‚ΠΎ ΠΈΠ· ΠΏΠΎΡ‚ΠΎΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Π° ΠΏΡ€ΠΈΠΉΡ‚ΠΈ minorVersion, ΠΎΠ΄Π½Π°ΠΊΠΎ считываниС происходит Π² ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΡƒΡŽ с ΠΈΠΌΠ΅Π½Π΅ΠΌ majorVersion. Π’Π΅ΠΌ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅, я Π½Π΅ Π·Π½Π°ΠΊΠΎΠΌ с ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»ΠΎΠΌ, Ρ‚Π°ΠΊ Ρ‡Ρ‚ΠΎ это лишь ΠΏΡ€Π΅Π΄ΠΏΠΎΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅.

Π€Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ 3

V524 It is odd that the body of ‘trio_index_last’ function is fully equivalent to the body of ‘trio_index’ function. triostr.c 933

/**
   Find first occurrence of a character in a string.
   ....
 */
TRIO_PUBLIC_STRING char *
trio_index
TRIO_ARGS2((string, character),
     TRIO_CONST char *string,
     int character)
{
  assert(string);
  return strchr(string, character);
}

/**
   Find last occurrence of a character in a string.
   ....
 */
TRIO_PUBLIC_STRING char *
trio_index_last
TRIO_ARGS2((string, character),
     TRIO_CONST char *string,
     int character)
{
  assert(string);
  return strchr(string, character);
}

Будя ΠΏΠΎ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΡŽ, функция trio_index Π½Π°Ρ…ΠΎΠ΄ΠΈΡ‚ ΠΏΠ΅Ρ€Π²ΠΎΠ΅ совпадСниС символа Π² строкС, ΠΊΠΎΠ³Π΄Π° trio_index_last β€” послСднСС. Но Ρ‚Π΅Π»Π° этих Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ‡Π½Ρ‹! Π‘ΠΊΠΎΡ€Π΅Π΅ всСго, это ΠΎΠΏΠ΅Ρ‡Π°Ρ‚ΠΊΠ°, ΠΈ Π² Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ trio_index_last Π½ΡƒΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ strrchr вмСсто strchr. Π’ΠΎΠ³Π΄Π° ΠΏΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΠΆΠΈΠ΄Π°Π΅ΠΌΡ‹ΠΌ.

Π€Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ 4

V769 The ‘data’ pointer in the expression equals nullptr. The resulting value of arithmetic operations on this pointer is senseless and it should not be used. nsc_encode.c 124

static BOOL nsc_encode_argb_to_aycocg(NSC_CONTEXT* context,
                                      const BYTE* data,
                                      UINT32 scanline)
{
  ....
  if (!context || data || (scanline == 0))
    return FALSE;
  ....
  src = data + (context->height - 1 - y) * scanline;
  ....
}

ΠŸΠΎΡ…ΠΎΠΆΠ΅, здСсь случайно пропустили ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€ отрицания ! рядом с data. Π‘Ρ‚Ρ€Π°Π½Π½ΠΎ, Ρ‡Ρ‚ΠΎ это ΠΎΡΡ‚Π°Π»ΠΎΡΡŒ Π½Π΅Π·Π°ΠΌΠ΅Ρ‡Π΅Π½Π½Ρ‹ΠΌ.

Π€Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ 5

V517 The use of ‘if (A) {…} else if (A) {…}’ pattern was detected. There is a probability of logical error presence. Check lines: 213, 222. rdpei_common.c 213

BOOL rdpei_write_4byte_unsigned(wStream* s, UINT32 value)
{
  BYTE byte;

  if (value <= 0x3F)
  {
    ....
  }
  else if (value <= 0x3FFF)
  {
    ....
  }
  else if (value <= 0x3FFFFF)
  {
    byte = (value >> 16) & 0x3F;
    Stream_Write_UINT8(s, byte | 0x80);
    byte = (value >> 8) & 0xFF;
    Stream_Write_UINT8(s, byte);
    byte = (value & 0xFF);
    Stream_Write_UINT8(s, byte);
  }
  else if (value <= 0x3FFFFF)
  {
    byte = (value >> 24) & 0x3F;
    Stream_Write_UINT8(s, byte | 0xC0);
    byte = (value >> 16) & 0xFF;
    Stream_Write_UINT8(s, byte);
    byte = (value >> 8) & 0xFF;
    Stream_Write_UINT8(s, byte);
    byte = (value & 0xFF);
    Stream_Write_UINT8(s, byte);
  }
  ....
}

ПослСдниС Π΄Π²Π° условия ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹: Π²ΠΈΠ΄ΠΈΠΌΠΎ, ΠΊΡ‚ΠΎ-Ρ‚ΠΎ Π·Π°Π±Ρ‹Π» ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ ΠΈΡ… послС копирования. По ΠΊΠΎΠ΄Ρƒ Π·Π°ΠΌΠ΅Ρ‚Π½ΠΎ, Ρ‡Ρ‚ΠΎ послСдняя Ρ‡Π°ΡΡ‚ΡŒ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ с Ρ‡Π΅Ρ‚Ρ‹Ρ€Π΅Ρ…Π±Π°ΠΉΡ‚Π½Ρ‹ΠΌΠΈ значСниями, поэтому ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡ€Π΅Π΄ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ послСднСС условиС Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ value <= 0x3FFFFFFF.

Π‘Ρ‹Π»Π° Π½Π°ΠΉΠ΄Π΅Π½Π° ΠΈ другая ошибка Ρ‚Π°ΠΊΠΎΠ³ΠΎ Ρ‚ΠΈΠΏΠ°:

  • V517 The use of ‘if (A) {…} else if (A) {…}’ pattern was detected. There is a probability of logical error presence. Check lines: 169, 173. file.c 169

ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π²Ρ…ΠΎΠ΄Π½Ρ‹Ρ… Π΄Π°Π½Π½Ρ‹Ρ…

Π€Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ 1

V547 Expression ‘strcat(target, source) != NULL’ is always true. 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 Expression ‘cache’ is always true. glyph.c 730

typedef struct rdp_glyph_cache rdpGlyphCache;

struct rdp_glyph_cache
{
  ....
  GLYPH_CACHE glyphCache[10];
  ....
};

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

  if (cache)
  {
    ....
  }
  ....
}

Π’ этом случаС ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ cache присваиваСтся адрСс статичСского массива glyphCache->glyphCache. Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΡƒ if (cache) ΠΌΠΎΠΆΠ½ΠΎ ΠΎΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ.

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

V1005 The resource was acquired using ‘CreateFileA’ function but was released using incompatible ‘fclose’ function. certificate.c 447

BOOL certificate_data_replace(rdpCertificateStore* certificate_store,
                              rdpCertificateData* certificate_data)
{
  HANDLE fp;
  ....
  fp = CreateFileA(certificate_store->file, GENERIC_READ | GENERIC_WRITE, 0,
                   NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  ....
  if (size < 1)
  {
    CloseHandle(fp);
    return FALSE;
  }
  ....
  if (!data)
  {
    fclose(fp);
    return FALSE;
  }
  ....
}

ДСскриптор Ρ„Π°ΠΉΠ»Π° fp, созданный Π²Ρ‹Π·ΠΎΠ²ΠΎΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ CreateFile, ΠΏΠΎ ошибкС Π·Π°ΠΊΡ€Ρ‹Ρ‚ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠ΅ΠΉ fclose ΠΈΠ· стандартной Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ, Π° Π½Π΅ CloseHandle.

ΠžΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹Π΅ условия

V581 The conditional expressions of the ‘if’ statements situated alongside each other are identical. Check lines: 269, 283. ndr_structure.c 283

void NdrComplexStructBufferSize(PMIDL_STUB_MESSAGE pStubMsg,
      unsigned char* pMemory, PFORMAT_STRING pFormat)
{
  ....
  if (conformant_array_description)
  {
    ULONG size;
    unsigned char array_type;
    array_type = conformant_array_description[0];
    size = NdrComplexStructMemberSize(pStubMsg, pFormat);
    WLog_ERR(TAG, "warning: NdrComplexStructBufferSize array_type: "
      "0x%02X unimplemented", array_type);
    NdrpComputeConformance(pStubMsg, pMemory + size,
      conformant_array_description);
    NdrpComputeVariance(pStubMsg, pMemory + size,
      conformant_array_description);
    MaxCount = pStubMsg->MaxCount;
    ActualCount = pStubMsg->ActualCount;
    Offset = pStubMsg->Offset;
  }

  if (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 unimplemented", array_type);
  }
  ....
}

Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, этот ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Π½Π΅ являСтся ошибкой. Однако ΠΎΠ±Π° условия содСрТат ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹Π΅ сообщСния, ΠΎΠ΄Π½ΠΎ ΠΈΠ· ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ…, скорСС всСго, ΠΌΠΎΠΆΠ½ΠΎ ΡƒΠ±Ρ€Π°Ρ‚ΡŒ.

ΠžΡ‡ΠΈΡΡ‚ΠΊΠ° Π½ΡƒΠ»Π΅Π²Ρ‹Ρ… ΡƒΠΊΠ°Π·Π°Ρ‚Π΅Π»Π΅ΠΉ

V575 The null pointer is passed into ‘free’ function. Inspect the first argument. smartcard_pcsc.c 875

WINSCARDAPI LONG WINAPI PCSC_SCardListReadersW(
  SCARDCONTEXT hContext,
  LPCWSTR mszGroups,
  LPWSTR mszReaders,
  LPDWORD pcchReaders)
{
  LPSTR mszGroupsA = NULL;
  ....
  mszGroups = NULL; /* mszGroups is not supported by pcsc-lite */

  if (mszGroups)
    ConvertFromUnicode(CP_UTF8,0, mszGroups, -1, 
                       (char**) &mszGroupsA, 0,
                       NULL, NULL);

  status = PCSC_SCardListReaders_Internal(hContext, mszGroupsA,
                                          (LPSTR) &mszReadersA,
                                          pcchReaders);

  if (status == SCARD_S_SUCCESS)
  {
    ....
  }

  free(mszGroupsA);
  ....
}

Π’ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ free ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ Π½ΡƒΠ»Π΅Π²ΠΎΠΉ ΡƒΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒ ΠΈ Π°Π½Π°Π»ΠΈΠ·Π°Ρ‚ΠΎΡ€ ΠΎΠ± этом Π·Π½Π°Π΅Ρ‚. Но Ссли выявляСтся ситуация, ΠΏΡ€ΠΈ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΉ ΡƒΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒ всСгда пСрСдаётся Π½ΡƒΠ»Π΅Π²Ρ‹ΠΌ, ΠΊΠ°ΠΊ Π² этом Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚Π΅, Π±ΡƒΠ΄Π΅Ρ‚ Π²Ρ‹Π΄Π°Π½ΠΎ ΠΏΡ€Π΅Π΄ΡƒΠΏΡ€Π΅ΠΆΠ΄Π΅Π½ΠΈΠ΅.

Π£ΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒ mszGroupsA ΠΈΠ·Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎ Ρ€Π°Π²Π΅Π½ NULL ΠΈ большС Π½ΠΈΠ³Π΄Π΅ Π½Π΅ инициализируСтся. ЕдинствСнная Π²Π΅Ρ‚Π²ΡŒ ΠΊΠΎΠ΄Π°, Π³Π΄Π΅ ΡƒΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒ ΠΌΠΎΠ³ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒΡΡ, являСтся нСдостиТимой.

Π‘Ρ‹Π»ΠΈ ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ сообщСния это Ρ‚ΠΈΠΏΠ°:

  • V575 The null pointer is passed into ‘free’ function. Inspect the first argument. license.c 790
  • V575 The null pointer is passed into ‘free’ function. Inspect the first argument. rdpsnd_alsa.c 575

Π‘ΠΊΠΎΡ€Π΅Π΅ всСго, ΠΏΠΎΠ΄ΠΎΠ±Π½Ρ‹Π΅ Π·Π°Π±Ρ‹Ρ‚Ρ‹Π΅ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ Π²ΠΎΠ·Π½ΠΈΠΊΠ°ΡŽΡ‚ Π² процСссС Ρ€Π΅Ρ„Π°ΠΊΡ‚ΠΎΡ€ΠΈΠ½Π³Π° ΠΈ ΠΈΡ… ΠΌΠΎΠΆΠ½ΠΎ просто ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ.

Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΠ΅ ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅

V1028 Possible overflow. Consider casting operands, not the result. makecert.c 1087

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

struct _MAKECERT_CONTEXT
{
  ....
  int duration_years;
  int duration_months;
};

typedef struct _MAKECERT_CONTEXT MAKECERT_CONTEXT;

int makecert_context_process(MAKECERT_CONTEXT* context, ....)
{
  ....
  if (context->duration_months)
    X509_gmtime_adj(after, (long)(60 * 60 * 24 * 31 *
      context->duration_months));
  else if (context->duration_years)
    X509_gmtime_adj(after, (long)(60 * 60 * 24 * 365 *
      context->duration_years));
  ....
}

ΠŸΡ€ΠΈΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π° ΠΊ long Π½Π΅ являСтся Π·Π°Ρ‰ΠΈΡ‚ΠΎΠΉ ΠΎΡ‚ пСрСполнСния, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ само вычислСниС происходит с использованиСм Ρ‚ΠΈΠΏΠ° int.

Π Π°Π·Ρ‹ΠΌΠ΅Π½ΠΎΠ²Π°Π½ΠΈΠ΅ указатСля Π² ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ

V595 The ‘context’ pointer was utilized before it was verified against nullptr. Check lines: 746, 748. gfx.c 746

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

  if (!context || !cmd)
    return ERROR_INVALID_PARAMETER;
  ....
}

Π—Π΄Π΅ΡΡŒ ΡƒΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒ context разымСновываСтся Π² ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ β€” Ρ€Π°Π½ΡŒΡˆΠ΅, Ρ‡Π΅ΠΌ происходит Π΅Π³ΠΎ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ°.

Π‘Ρ‹Π»ΠΈ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹ ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ошибки Ρ‚Π°ΠΊΠΎΠ³ΠΎ Ρ‚ΠΈΠΏΠ°:

  • V595 The ‘ntlm’ pointer was utilized before it was verified against nullptr. Check lines: 236, 255. ntlm.c 236
  • V595 The ‘context’ pointer was utilized before it was verified against nullptr. Check lines: 1003, 1007. rfx.c 1003
  • V595 The ‘rdpei’ pointer was utilized before it was verified against nullptr. Check lines: 176, 180. rdpei_main.c 176
  • V595 The ‘gdi’ pointer was utilized before it was verified against nullptr. Check lines: 121, 123. xf_gfx.c 121

БСссмыслСнноС условиС

V547 Expression ‘rdp->state >= CONNECTION_STATE_ACTIVE’ is always true. connection.c 1489

int rdp_server_transition_to_state(rdpRdp* rdp, int state)
{
  ....
  switch (state)
  {
    ....
    case CONNECTION_STATE_ACTIVE:
      rdp->state = CONNECTION_STATE_ACTIVE;          // <=
      ....
      if (rdp->state >= CONNECTION_STATE_ACTIVE)     // <=
      {
        IFCALLRET(client->Activate, client->activated, client);

        if (!client->activated)
          return -1;
      }
    ....
  }
  ....
}

Π›Π΅Π³ΠΊΠΎ Π·Π°ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΏΠ΅Ρ€Π²ΠΎΠ΅ условиС Π½Π΅ ΠΈΠΌΠ΅Π΅Ρ‚ смысла ΠΈΠ·-Π·Π° присваивания ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π³ΠΎ значСния Ρ€Π°Π½Π΅Π΅.

НСкоррСктный Ρ€Π°Π·Π±ΠΎΡ€ строки

V576 Incorrect format. Consider checking the third actual argument of the ‘sscanf’ function. A pointer to the unsigned int type is expected. proxy.c 220

V560 A part of conditional expression is always true: (rc >= 0). proxy.c 222

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

  if ((rc == 1) && (rc >= 0))
  {
    ....
  }
  ....
}

Анализатор для этого Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚Π° Π²Ρ‹Π΄Π°Π΅Ρ‚ сразу 2 прСдупрСТдСния. Π‘ΠΏΠ΅Ρ†ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ %u ΠΎΠΆΠΈΠ΄Π°Π΅Ρ‚ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΡƒΡŽ Ρ‚ΠΈΠΏΠ° unsigned int, Π½ΠΎ пСрСмСнная sub ΠΈΠΌΠ΅Π΅Ρ‚ Ρ‚ΠΈΠΏ int. Π”Π°Π»Π΅Π΅ ΠΌΡ‹ Π²ΠΈΠ΄ΠΈΠΌ ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΡƒ: условиС справа Π½Π΅ ΠΈΠΌΠ΅Π΅Ρ‚ смысла, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Π² Π½Π°Ρ‡Π°Π»Π΅ ΠΈΠ΄Π΅Ρ‚ сравнСниС с Π΅Π΄ΠΈΠ½ΠΈΡ†Π΅ΠΉ. НС знаю, Ρ‡Ρ‚ΠΎ ΠΈΠΌΠ΅Π» Π² Π²ΠΈΠ΄Ρƒ Π°Π²Ρ‚ΠΎΡ€ этого ΠΊΠΎΠ΄Π°, Π½ΠΎ Ρ‚ΡƒΡ‚ явно Ρ‡Ρ‚ΠΎ-Ρ‚ΠΎ Π½Π΅ Ρ‚Π°ΠΊ.

НСупорядочСнныС ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ

V547 Expression ‘status == 0x00090314’ is always false. ntlm.c 299

BOOL ntlm_authenticate(rdpNtlm* ntlm, BOOL* pbContinueNeeded)
{
  ....
  if (status != SEC_E_OK)
  {
    ....
    return FALSE;
  }

  if (status == SEC_I_COMPLETE_NEEDED)            // <=
    status = SEC_E_OK;
  else if (status == SEC_I_COMPLETE_AND_CONTINUE) // <=
    status = SEC_I_CONTINUE_NEEDED;
  ....
}

ΠžΡ‚ΠΌΠ΅Ρ‡Π΅Π½Π½Ρ‹Π΅ условия Π±ΡƒΠ΄ΡƒΡ‚ всСгда Π»ΠΎΠΆΠ½Ρ‹, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Π΄ΠΎΠΉΠ΄Π΅Ρ‚ Π΄ΠΎ Π²Ρ‚ΠΎΡ€ΠΎΠ³ΠΎ условия Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π² Ρ‚ΠΎΠΌ случаС, ΠΊΠΎΠ³Π΄Π° status == SEC_E_OK. ΠŸΡ€Π°Π²ΠΈΠ»ΡŒΠ½Ρ‹ΠΉ ΠΊΠΎΠ΄ ΠΌΠΎΠΆΠ΅Ρ‚ Π²Ρ‹Π³Π»ΡΠ΄Π΅Ρ‚ΡŒ Ρ‚Π°ΠΊ:

if (status == SEC_I_COMPLETE_NEEDED)
  status = SEC_E_OK;
else if (status == SEC_I_COMPLETE_AND_CONTINUE)
  status = SEC_I_CONTINUE_NEEDED;
else if (status != SEC_E_OK)
{
  ....
  return FALSE;
}

Π—Π°ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅

Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° выявила мноТСство ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌ, Π½ΠΎ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π°ΠΈΠ±ΠΎΠ»Π΅Π΅ интСрСсная ΠΈΡ… Ρ‡Π°ΡΡ‚ΡŒ Π±Ρ‹Π»Π° описана Π² ΡΡ‚Π°Ρ‚ΡŒΠ΅. Π Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° ΠΌΠΎΠ³ΡƒΡ‚ сами ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚, запросив Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ Π»ΠΈΡ†Π΅Π½Π·ΠΈΠΈ Π½Π° сайтС PVS-Studio. Π‘Ρ‹Π»ΠΈ Ρ‚Π°ΠΊΠΆΠ΅ ΠΈ Π»ΠΎΠΆΠ½Ρ‹Π΅ срабатывания, Ρ€Π°Π±ΠΎΡ‚Π° Π½Π°Π΄ ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌΠΈ ΠΏΠΎΠΌΠΎΠΆΠ΅Ρ‚ ΡƒΠ»ΡƒΡ‡ΡˆΠΈΡ‚ΡŒ Π°Π½Π°Π»ΠΈΠ·Π°Ρ‚ΠΎΡ€. Π’Π΅ΠΌ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅, статичСский Π°Π½Π°Π»ΠΈΠ· Π²Π°ΠΆΠ΅Π½, Ссли Π²Ρ‹ Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΠΎΠ²Ρ‹ΡΠΈΡ‚ΡŒ качСство ΠΊΠΎΠ΄Π°, Π½ΠΎ ΠΈ ΡΠΎΠΊΡ€Π°Ρ‚ΠΈΡ‚ΡŒ врСмя Π½Π° поиск ошибок, ΠΈ PVS-Studio ΠΌΠΎΠΆΠ΅Ρ‚ Π² этом ΠΏΠΎΠΌΠΎΡ‡ΡŒ.

ο»ΏΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° FreeRDP с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Π°Π½Π°Π»ΠΈΠ·Π°Ρ‚ΠΎΡ€Π° PVS-Studio

Если Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ ΠΏΠΎΠ΄Π΅Π»ΠΈΡ‚ΡŒΡΡ этой ΡΡ‚Π°Ρ‚ΡŒΠ΅ΠΉ с англоязычной Π°ΡƒΠ΄ΠΈΡ‚ΠΎΡ€ΠΈΠ΅ΠΉ, Ρ‚ΠΎ ΠΏΡ€ΠΎΡˆΡƒ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ссылку Π½Π° ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄: Sergey Larin. Checking FreeRDP with PVS-Studio

Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊ: habr.com

Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ