5 октября 2022 г.

EurekaLog помогает не только вам, но и разработчикам используемых вами библиотек

С нами связался клиент, который утверждал, что его приложение отлично работало, пока он не добавил в него EurekaLog. Конкретнее, его приложение начало выбрасывать исключение EAccessViolation при выполнении кода определённого события.

Беглое знакомство с проектом показало, что приложение возбуждало исключение EAccessViolation с текстом "Access violation at 0x0108164d: read of address 0xdeadc30f" и таким стеком вызовов:
ContosoInplaceContainer.TContosoCustomViewInfoItem.Destroy
ContosoInplaceContainer.TContosoEditCellViewInfo.Destroy
System.TObject.Free
ContosoClasses.TContosoObjectList.FreeItem
ContosoClasses.TContosoObjectList.Clear
ContosoGrid.TContosoCustomRowViewInfo.ClearValuesInfo
ContosoGrid.TContosoCustomMultiEditorRow.EditorsChanged
ContosoGrid.TContosoEditorPropertiesCollection.Update
System.Classes.TCollection.Changed
System.Classes.TCollection.RemoveItem
System.Classes.TCollectionItem.SetCollection
System.Classes.TCollectionItem.Release
System.Classes.TCollectionItem.Destroy
ContosoGrid.TContosoCustomEditorRowProperties.Destroy
System.TObject.Free
System.Classes.TCollection.Delete
Unit2.TForm2.DeleteEditor
Unit2.TForm2.PropertiesEditValueChanged
ContosoEdit.TContosoCustomEditingController.EditValueChanged
ContosoInplaceContainer.TContosoEditingController.EditValueChanged
...
Проблемный код выглядел так:
destructor TContosoCustomViewInfoItem.Destroy;
begin
  // Вылетает в строке ниже:
  if (Control <> nil) and (Control.Controller.HotTrackController.HintElement = Self) then 
    Control.Controller.HotTrackController.CancelHint;
  inherited Destroy;
end;
Здесь слово "Contoso" ссылается на Contoso Ltd. - фиктивную (выдуманную) компанию, используемую Microsoft в качестве примеров. В данном случае это слово скрывает настоящего разработчика библиотеки, поскольку смысл этой заметки в том, чтобы показать, как EurekaLog помогает отыскивать проблемы в коде, а не в том, чтобы тыкать в кого-то пальцем.

В отличие от большинства подобных историй, этот случай было достаточно просто расследовать - не в последнюю очередь благодаря тому, что у нас был надёжно воспроизводимый пример.

Во-первых, обратите внимение, что сообщение исключения выглядит так: "...read of address 0xdeadc30f". Заметьте, что какой-то код пытается что-то прочитать по адресу DEADC30F, который лежит очень близко к адресу DEADBEEF. Адрес DEADBEEF - это специальный отладочный маркер (см. описание опции "When memory is released"), которым заполняется освобождённая память. Иными словами, этот код пытается читать из уже удалённого объекта.

Вышесказанное означает, что в коде клиента (или сторонней библиотеке Contoso) есть баг типа "use after free" (доступ к удалённой памяти). Этот вывод 100% точен, эта ситуация НЕ может быть ложно-положительной. Другими словами, если клиент уверен, что его код корректен - то он только что нашёл баг в сторонней библиотеке Contoso (наши поздравления!). И наоборот: если разработчики Contoso утверждают, что их код корректен, то баг сидит в коде клиента (то, как он работает с библиотекой Contoso).

Вооружённые этим знанием, нам теперь осталось лишь просто пройтись по коду клиента, уделяя внимание удаляемым объектам. Вот как проходит выполнение:
  1. Метод TContosoCustomEditingController.EditValueChanged вызывает обработчик события, который назначен на код клиента. Обработчик события клиента (PropertiesEditValueChanged) вызывает Row1.Properties.Editors.Delete(1);.
  2. Метод Delete здесь - это обычный метод TCollection из RTL. Он удаляет элемент из коллекции:
    procedure TCollection.Delete(Index: Integer);
    begin
      Notify(TCollectionItem(FItems[Index]), cnDeleting);
      TCollectionItem(FItems[Index]).DisposeOf; // здесь
    end;
    
    destructor TContosoCustomEditorRowProperties.Destroy;
    begin
      FreeAndNil(FEditContainer); // ВАЖНО
      inherited Destroy; // здесь
    end;
    Обратите внимение, что метод TContosoCustomEditorRowProperties.Destroy удаляет (своё) поле FEditContainer. Однако на этот объект всё ещё есть ссылка в другом месте - как мы увидем ниже.
  3. Теперь деструктор TCollectionItem отсоединяет себя от владельца-коллекции:
    destructor TCollectionItem.Destroy;
    begin
      if FCollection <> nil then
        Release;  // здесь
      inherited Destroy;
    end;
    
    procedure TCollectionItem.Release;
    begin
      SetCollection(nil); // здесь
    end;
    
    procedure TCollectionItem.SetCollection(Value: TCollection);
    begin
      if FCollection <> Value then
      begin
        if FCollection <> nil then FCollection.RemoveItem(Self); // здесь
        if Value <> nil then Value.InsertItem(Self);
      end;
    end;
    
    procedure TCollection.RemoveItem(Item: TCollectionItem);
    begin
      Notify(Item, cnExtracting);
      if Item = FItems.Last then
        FItems.Delete(FItems.Count - 1)
      else
        FItems.Remove(Item);
      Item.FCollection := nil;
      NotifyDesigner(Self, Item, opRemove);
      Changed; // здесь
    end;
  4. Как часть этого процесса: будут запущены обработчики уведомлений, в частности, включая TContosoEditorPropertiesCollection.Update:
    procedure TCollection.Changed;
    begin
      if FUpdateCount = 0 then Update(nil); // здесь
    end;
    
    procedure TContosoEditorPropertiesCollection.Update(Item: TCollectionItem);
    var
      I: Integer;
    begin
      for I := 0 to Count - 1 do
        GetItem(I).EditContainer.FCellIndex := I;
      Row.EditorsChanged; // здесь
    end;
  5. Далее библиотека Contoso попытается обновить редакторы:
    procedure TContosoCustomMultiEditorRow.EditorsChanged;
    begin
      if Properties.Locked or VerticalGrid.IsLoading then Exit;
      ViewInfo.ClearValuesInfo; // здесь
      Changed;
    end;
    
    procedure TContosoCustomRowViewInfo.ClearValuesInfo;
    begin
      FIsRightToLeftConverted := False;
      FInitialized := False;
      ValuesInfo.Clear; // здесь
      ValuesLinesInfo.Clear;
    end;
    
    procedure TContosoObjectList.Clear;
    var
      I: Integer;
    begin
      if OwnObjects then
      begin
        for I := 0 to Count - 1 do // = 2
          FreeItem(I); // здесь
      end;
      inherited Clear;
    end;
  6. В списке ValuesInfo находится два элемента. Первый (с индексом 0) удаляется успешно, а вот второй (с индексом 1) и вызывает проблему:
    destructor TContosoEditCellViewInfo.Destroy;
    begin
      if (EditContainer <> nil) and not EditContainer.IsDestroying then // здесь
      // ...
    end;
    Вот и проблема: EditContainer является Owner-ом и он указывает на уже удалённый объект - то самое поле, удалённое на шаге 2 внутри TContosoCustomEditorRowProperties.Destroy.
Краткий вывод:
  1. Код клиента удаляет элемент из коллекции;
  2. Элемент удаляет свои поля;
  3. Коллекция уведомляет об удалении элемента;
  4. Код уведомления пытается очистить ассоциированную информацию и в процессе пытается читать из уже удалённого поля/объекта.
Вот и наш баг: доступ к уже удалённому объекту.

Вы можете спросить: а как же код работал "безупречно" до добавления EurekaLog в проект? Ну это просто: удалённый объект оставался в памяти без изменений, поэтому TContosoEditCellViewInfo.Destroy мог успешно получить доступ к уже удалённому объекту и прочитать из него неизменённые данные. Добавление EurekaLog в проект с настройками по умолчанию (включенные проверки памяти) изменяет это поведение, приводя к настоящему стиранию удаленных объектов/памяти.

Итак, является ли это ошибкой в библиотеке Contoso? Хотя это, безусловно, выглядит именно так, мы не можем быть уверены в этом на 100%, так как не являемся экспертами в этой библиотеке. Так что вполне возможно, что это ошибка в коде клиента.

Клиент сообщил, что эта проблема будет передана в службу поддержки библиотеки Contoso.

Комментариев нет :

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.