# V3090. Unsafe locking on an object.

Анализатор обнаружил фрагмент кода с использованием небезопасной блокировки.

Данная диагностика срабатывает на ряд случаев:

- использование 'this' в качестве объекта блокировки;
- использование в качестве объекта блокировки экземпляра класса 'Type', 'MemberInfo', 'ParameterInfo', 'String', 'Thread';
- использование в качестве объекта блокировки публичного члена текущего класса;
- использование в качестве объекта блокировки объекта, полученного в результате упаковки;
- блокировка происходит по вновь создаваемым объектам.

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

Блокировка с использованием 'this' небезопасна, если класс не является приватным. Тогда в другом месте программист может установить блокировку на этот же объект после создания его экземпляра.

Аналогичная ситуация с публичными членами классов.

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

Приведём пример небезопасного кода с использованием оператора 'lock' и 'this' в качестве объекта блокировки:

    class A
    {
      void Foo()
      {
        lock(this)
        {
          // do smt
        }
      }
    }

Для того, чтобы избежать возможных взаимных блокировок, в качестве объекта блокировки стоит использовать, например, приватное поле:

    class A
    {
      private Object locker = new Object();
      void Foo()
      {
        lock(locker)
        {
          // do smt
        }
      }
    }

С экземплярами класса 'Type', 'MemberInfo', 'ParameterInfo' ситуация несколько опаснее, так как здесь вероятность взаимной блокировки выше. Используя оператор 'typeof' или метод 'GetType', метод 'GetMember' и т.п. для разных экземпляров одного типа, результат будет одинаков - один и тот же экземпляр данного класса.

Отдельно стоят объекты типов 'String' и 'Thread'.

Доступ к объектам этих типов можно получить из любого места в программе, и даже из другого домена приложения (Application Domain), что ещё больше увеличивает опасность взаимной блокировки. Решение - не использовать в качестве объектов блокировки экземпляры этих типов.

Рассмотрим пример возникновения взаимной блокировки. Пусть имеется следующий код в каком-нибудь приложении (Sample.exe):

    static void Main(string[] args)
    {
      var thread = new Thread(() => Process());
      thread.Start();
      thread.Join();
    }
    static void Process()
    {
      String locker = "my locker";
      lock (locker)
      {
        ....
      }
    }

В другом приложении есть код следующего вида:

    String locker = "my locker";
    lock (locker)
    {
      AppDomain domain = AppDomain.CreateDomain("test");
      domain.ExecuteAssembly(@"C:\Sample.exe");
    }

Результатом выполнения этого кода будет взаимная блокировка, возникшая в результате использования в качестве объекта блокировки экземпляра класса 'String'.

Мы создаём новый домен в рамках того же процесса и пытаемся выполнить в нём сборку, содержащуюся в другом файле (Sample.exe). В итоге возникает ситуация, когда оба оператора 'lock' используют в качестве объекта блокировки одинаковый строковый литерал. Для строковых литералов срабатывает механизм интернирования строк, за счёт которого в обоих случаях будут получены ссылки на один и тот же объект в памяти. Как следствие - оба оператора 'lock' выполняют блокировку по одному и тому же объекту, что и привело к взаимоблокировке.

Эта ошибка могла бы проявить себя и при исполнении в рамках одного домена.

Аналогичная ситуация с типом 'Thread', экземпляр которого можно легко получить, например, с помощью свойства 'Thread.CurrentThread'.

Решение - не использовать в качестве объектов блокировки объекты типов 'Thread' и 'String'.

Блокировка с использованием объекта значимого типа приведёт к тому, что синхронизация потоков осуществляться не будет. Стоит отметить, что конструкция 'lock' не позволяет выполнять блокировку на объектах значимого типа, но класс 'Monitor' с его методами 'Enter' и 'TryEnter' от этого не застрахован.

Методы 'Enter' и 'TryEnter' ожидают в качестве параметра объект типа 'Object', поэтому, если в метод передаётся объект значимого типа, будет выполнена его 'упаковка'. Это значит, что каждый раз для блокировки будет создаваться новый объект, следовательно - блокировка будет устанавливаться (и сниматься) по этому новому объекту. Результат - отсутствие желаемой синхронизации.

Рассмотрим пример ошибочного кода:

    sealed class A
    {
      private Int32 m_locker = 10;

      void Foo()
      {
        Monitor.Enter(m_locker);
        // Do smt...
        Monitor.Exit(m_locker);
      }
    }

Программист хотел установить блокировку по приватному полю 'm_locker'. На самом же деле блокировка будет устанавливаться (и сниматься) не по желаемому полю, а по вновь созданным объектам, полученным в результате 'упаковки'.

Для исправления данной ошибки достаточно изменить тип поля 'm_locker' на допустимый ссылочный, например - 'Object'. Тогда пример корректного кода выглядел бы так:

    sealed class A
    {
      private Object m_locker = new Object();

      void Foo()
      {
        Monitor.Enter(m_locker);
        // Do smt...
        Monitor.Exit(m_locker);
      }
    }

Схожая ошибка проявится и при использовании конструкции 'lock', если выполняется упаковка объекта в результате приведения:

    Int32 val = 10;
    lock ((Object)val)
    { .... }

В этом коде блокировка будет устанавливаться по объектам, полученным в результате упаковки. Так как в результате упаковки будут создаваться новые объекты, синхронизации потоков не будет.

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

    lock (new Object())
    { .... }

или так

    lock (obj = new Object())
    { .... }

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

Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.).

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

CWE-662 CWE-833

Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3090.