Еще раз об утечке атомов и баге VCL

Введение

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

В статье MrShoor описывается переполнение т.н. таблицы атомов в случае, если приложение на Delphi завершено некорректно, и некоторые атомы не удаляются. Оказывается, для переполнения таблицы атомов вовсе необязательно «убивать насильно» ваше приложение. Вполне достаточно его запустить и корректно закрыть, но много-много раз подряд.
Давайте посмотрим, как это происходит:

Описание механизма переполнения

После получения очередной жалобы а-ля «out of memory», мы обнаружили, что таблица атомов забита элементами вида ControlOfs<шестнадцатеричный ID>. Эти элементы появляются при запуске каждого приложения (а наш сервер приложений запускает экземпляры по одному для каждого содинения), и остаются в таблице навсегда.

Рассмотрим еще раз участок кода из InitControls в Controls.pas — тот же самый, что и в упомянутой выше статье:

  WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]);
  WindowAtom := GlobalAddAtom(PChar(WindowAtomStrinjg));
  ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]);
  ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
  RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));


Самая последня строка, как казалось, тоже создает атом. При регистрации нового типа сообщения также в таблицу добавляется новый атом, который каждый раз имеет новое имя. Проблема в том, что RegisterWindowsMessage см. MSDN в принципе не предполагает обратного unregister действия, т.к. данная функция позволяет нескольким программам использовать некоторый Message ID совместно:
The RegisterWindowMessage function is typically used to register messages for communicating between two cooperating applications.

If two different applications register the same message string, the applications return the same message value. The message remains registered until the session ends.

Это уже серьезно, т.к. у нас нет никаких рычагов влияния на ситуацию. Закрыть такой атом сторонним приложением невозможно — программа, предложенная в статье, побудившей меня к написанию данного поста, здесь бессильна. Так как таблица атомов имеет корни в 16 битной эпохе, что налагает ограничение на размер таблицы, то этот досадный баг довольно быстро выводил серверную часть из строя, т.к. не было возможности запустить ни одно Delphi приложение без перезагрузки системы.

Вот пример того, как выглядит таблица атомов после 20 итераций «нормальный запуск-корректное завершение» для простой программы из 1 окна:

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

Эта ошибка была описана в нескольких местах, например:
Детальное описание ошибки
Более краткий bug report
Ну, и конечно же, Stack Overflow

Метод решения

Поскольку удалить атом пост-фактум нельзя, требуется не допустить его создания. Наша команда пошла по пути IAT хука, который перехватывает вызов к RegisterWindowMessageA и в случае, если регистрируется сообщение с именем вида ControlOfs<что-то там>, вместо него регистрируется любой другой идентификатор, который одинаков для всех приложений. Как оказалось, ему вовсе не обязательно было быть уникальным, на что также указывается в баг репорте, на который я уже ссылался.

Код хука тривиален, как и механизм установки его. Более того, в интернете есть готовые библиотеки для Delphi для хуков на IAT. Сам хук всего навсего проверяет максимально быстрым способом, не соответствует ли регистрируемое сообщение префиксу ControlOfs, и, в случае, если соответствует, происходит подмена на RM_GetObjectInstance — подобный идентификатор, одинаковый для всех приложений, с вызовом оригинальной RegisterWindowMessage.

Надеюсь, кому-то это поможет избежать долгой и трудной отладки.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 12

    0
    Код хука тривиален


    Если так все тривиально, то наверно следовало бы привести хоть ссылки на этот код, а то одни слова…
      0
      Касаемо перехвата:

      Это действительно тривиальный код — имея под руками SDK .h заголовки, процедура перехвата пишется за пол-часа.
      Готовое решение можно посмотреть здесь: проект uall.
      Или просто гуглить по «delphi iat hook», первые 3 результата — вполне.
      Свой код привести не могу, часть коммерческого продукта, извините.

      Сам код перехватчика в простейшем варианте примерно таков:
      function myRegisterWindowMessageA(name: PAnsiChar): UINT; stdcall;
      ...
      ...
      len := lstrlen(name);
      if copy(name, 1, 10) = 'ControlOfs' then
        Result := OriginalRegisterWindowMessageA(PChar('abcd'));
      else
        Result := OriginalRegisterWindowMessageA(Name);
      
      

      Но его можно допилить для повышения эффективности.
    • НЛО прилетело и опубликовало эту надпись здесь
        0
        В принципе, это здравая идея, но во-первых, перекомпилировать самому RTL не всегда безопасно, могут быть какие-то отличия в директивах и пр. Положиться на борландовскую RTL можно, ее все используют, и большинство ошибок отловлено. Во-вторых, могут возникнуть проблемы вида «unit was compiled with different...», т.е. объем перекомпиляции может быть большим. Ради исправления одной инструкции это нецелесообразно.

        Так да — строку
        RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));
        Надо заменить на
        RM_GetObjectInstance := RegisterWindowMessage(PChar('ControlAtomString'));

        Если кто-то соберется перекомпилировать библиотеки, могут понадобиться ключи для system.pas и подобных:

        dcc32.exe system.pas sysinit.pas strings.pas -JP -M -Y -Z -$D- -0 > sysbuild.log
          0
          Лучше использовать строку 'RM_GetObjectInstance' — просто потому, что так описано на Embarcadero Quality Center. Чтобы поменьше разброда было.
            0
            Насколько я помню, как раз в Delphi 7 был неприятный баг (гонка) в WaitForThreads. Хотите совсем без багов — используйте Delphi 5.
              0
              Не приходилось использовать WaitForThreads, через WaitForMultipleObjects работаем. 7ка — и то старье, 5ка — вообще древность. Решим на работе проблему с Unicode строками, перейдем на XE.
          0
          Замечательная бага. Правда «пофикшена» в XE5 по крайней мере точно:
          RM_GetObjectInstance := RegisterWindowMessage(PChar('DelphiRM_GetObjectInstance'));
          Удивительно, что по факту RM_GetObjectInstance вообще не нужен. Эта переменная лежит в имплементации юнита Control.pas и используется только в одном месте:
          function ObjectFromHWnd(Handle: HWnd): TWinControl;
          var
            OwningProcess: DWORD;
            ProcessId: DWORD;
          begin
            ProcessId := GetCurrentProcessId;
            if (GetWindowThreadProcessId(Handle, OwningProcess) <> 0) and
               (OwningProcess = ProcessId) then
              Result := Pointer(SendMessage(Handle, RM_GetObjectInstance, ProcessId, 0))
            else
              Result := nil;
          end;

          ObjectFromHWnd так же только в implementation, и ниоткуда снаружи не вызывается. В самом Controls.pas он вызывается дважды:
          function FindControl(Handle: HWnd): TWinControl;
          var
            OwningProcess: DWORD;
          begin
            Result := nil;
            if (Handle <> 0) and (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and
               (OwningProcess = GetCurrentProcessId) then
            begin
              if GlobalFindAtom(PChar(ControlAtomString)) = ControlAtom then
                Result := Pointer(GetProp(Handle, MakeIntAtom(ControlAtom)))
              else
                Result := ObjectFromHWnd(Handle);
            end;
          end;

          function IsDelphiHandle(Handle: HWND): Boolean;
          var
            OwningProcess: DWORD;
          begin
            Result := False;
            if (Handle <> 0) and (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and
               (OwningProcess = GetCurrentProcessId) then
            begin
          {$IF DEFINED(CLR)}
              Result := FindControl(Handle) <> nil;
          {$ELSE}
              if GlobalFindAtom(PChar(WindowAtomString)) = WindowAtom then
                Result := GetProp(Handle, MakeIntAtom(WindowAtom)) <> 0
              else
                Result := ObjectFromHWnd(Handle) <> nil;
          {$ENDIF}
            end;
          end;

          В обоих случаях он вызывается после проверок:
          GlobalFindAtom(PChar(ControlAtomString)) = ControlAtom
          GlobalFindAtom(PChar(WindowAtomString)) = WindowAtom
          И обе эти проверки всегда возвращают TRUE. Следовательно до вызова ObjectFromHWnd вообще никогда не доходит, а следовательно RM_GetObjectInstance вообще не нужен. Так что можете даже ничего не менять в хуке, а просто забивать болт если там есть 'ControlOfs'

          Кстати в MSDN не сказано что винда не чистит атомы от RegisterWindowMessage. Я проверил на WIN7 со всеми обновлениями. Сожрал атомы и прибил процесс. Через некоторое время винда очистила хендлы, но очистила их далеко не сразу после регистрации. Так что возможно в WIN7 такая ситуация с утечкой от RegisterWindowMessage не возникает вообще. Если конечно не запускать 16К приложений делфи одновременно. :) А поскольку RM_GetObjectInstance не используется — то приложению от того что оно не смогло регнуть себе мессагу для RM_GetObjectInstance — должно быть не холодно не жарко. Должно бы быть, но это не так. Какого-то лешего разработчики Windows решили, что в принципе достаточно использовать один скоуп для RegisterClipboardFormat и RegisterWindowMessage. Соответственно если вдруг закончится эта таблица — ни одно приложение не сможет регнуть свой формат буфера обмена. Да и потом, неудача с RM_GetObjectInstance VCL не огорчит, а вот все остальные приложения в ОС будут вести себя нестабильно. Ситуацию спасает лишь то, что винда периодически (и неизвестно при каких обстоятельствах) чистит эти атомы.
            0
            ObjectFromhWnd выглядит как костыль — получение дельфийского объекта из виндового хэндла. Видимо, использовался в древности для связи WinAPI — gui кода и VCL.
            Промахнулся с ответом, извините — внизу простыня -вторая часть ответа.
            0
            Да, именно. Используется только в одном месте, поэтому можно использовать тупо константу.

            Кстати в MSDN не сказано что винда не чистит атомы от RegisterWindowMessage

            В MSDN сказано, как я цитировал в посте:
            The message remains registered until the session ends.


            Я проверил на WIN7 со всеми обновлениями. Сожрал атомы и прибил процесс. Через некоторое время винда очистила хендлы, но очистила их далеко не сразу после регистрации

            Атомы вообще или именно RWMовские?
            На WS2008 нет очистки, мы именно на 2008м увидели это впервые. И атомы накапливались там не один день.

            Какого-то лешего разработчики Windows решили, что в принципе достаточно использовать один скоуп для RegisterClipboardFormat и RegisterWindowMessage

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

            Да и потом, неудача с RM_GetObjectInstance VCL не огорчит

            Ну, как вам сказать — сообщение-то это VCL не нужно, но вот приложение не запустится. Вообще.

            Ситуацию спасает лишь то, что винда периодически (и неизвестно при каких обстоятельствах) чистит эти атомы.

            Я, к сожалению, не могу этого подтвердить. MSDN утверждает, что RWM атомы живут до конца сессии, и именно это я наблюдаю.
              0
              В MSDN сказано, как я цитировал в посте:
              Упс, не заметил конца фразы.
              Атомы вообще или именно RWMовские?
              Именно RWM-овские. Чистилось не сразу, а через некоторое время, после того как заканчивались. Т.е. я в цикле гоняю пачку RegisterWindowMessage с рандомными именами. Когда начинает возвращать 0 — прекращаю цикл и завершаю приложение. Таким образом у меня все атомы съедены. После этого поперезапускаю процессы (там IDE перезапущу, или еще какие тяжелые процессы), и атомы вновь появляются. По крайней мере RegisterWindowMessage начинает возвращать не ноль.
              На WS2008 нет очистки, мы именно на 2008м увидели это впервые. И атомы накапливались там не один день.
              Это очень печально, потому что RegisterWindowMessage сделан костылем через клипборд.
              Ну, как вам сказать — сообщение-то это VCL не нужно, но вот приложение не запустится. Вообще.
              А у меня все запускается. Там возвращается 0, ну и дальше все типтоп.
                0
                Тут надо отметить следующее: 1)в моем случае каждое приложение регистрировало по одному RWM атому, не пачкой; 2)Приложения запускались в non-interractive session, из-под сервиса, т.е. от SYSTEM.
                Может быть, в этом разница.
                А у меня все запускается. Там возвращается 0, ну и дальше все типтоп.

                Эм, я не совсем правильно выразился. Да, 0, но просто в последствии там еще потребуются атомы при инциализации графики, и initialization какого-то из модулей просто вылетает с out of resources. Не помню последовательность, но после controls еще что-то графическое будет инициализироваться точно.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое