05 October, 2022

EurekaLog helps not only you, but also developers of libraries that you are using

We were contacted by a customer that claimed that his application worked fine until he added EurekaLog to it. Specifically, his application starts to raise an EAccessViolation exception in his event handler's code.

A quick look indicated that the EAccessViolation exception was thrown with the following message: "Access violation at 0x0108164d: read of address 0xdeadc30f" and the following call stack:
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
...
The code in question is:
destructor TContosoCustomViewInfoItem.Destroy;
begin
  // Crashes below:
  if (Control <> nil) and (Control.Controller.HotTrackController.HintElement = Self) then 
    Control.Controller.HotTrackController.CancelHint;
  inherited Destroy;
end;
The "Contoso" refers to Contoso Ltd. - which is a fictional company used by Microsoft as an example company. Here it masks the real vendor of a certain well-known 3rd party library, as the point of this story is to show how EurekaLog can help you find bugs, not to ridicule any particular developer/vendor.

Unlike most other stories this one was extremely easy to resolve. Mostly because we had a reliable reproducible example.

First of all, take a closer look at the exception message: "...read of address 0xdeadc30f". Notice that the code is trying to access the DEADC30F address, which is close to the DEADBEEF address. The DEADBEEF is a special debugging marker (see the "When memory is released" option), indicating already released memory. In other words, this code is trying to access already deleted object.

The above means that our customer (or 3rd pary code, e.g. Contoso) have "use after free" bug somewhere. This is 100% reliable information, which can NOT be false-positive. In other words, if customer is sure that his code is correct - then he has found a bug in the Contoso library (congratulations!). And visa versa: if Contoso library code is correct, then there is a bug in customer's code on how he uses the Contoso library.

Armed with this information all there is left to do is to simply walk through customer's code, paying attention to all delete/free operations. Here is how it goes:
  1. The TContosoCustomEditingController.EditValueChanged fires an event handler, which is set to customer's code (PropertiesEditValueChanged), which calls: Row1.Properties.Editors.Delete(1);.
  2. The Delete is a method of RTL's TCollection, which removes item from the collection and then deletes the item:
    procedure TCollection.Delete(Index: Integer);
    begin
      Notify(TCollectionItem(FItems[Index]), cnDeleting);
      TCollectionItem(FItems[Index]).DisposeOf; // here
    end;
    
    destructor TContosoCustomEditorRowProperties.Destroy;
    begin
      FreeAndNil(FEditContainer); // IMPORTANT
      inherited Destroy; // here
    end;
    Notice that the TContosoCustomEditorRowProperties.Destroy is deleting the FEditContainer field. However, there is a reference to that object in other place - as we will see below.
  3. Now TCollectionItem's destructor will release itself from the owner (collection):
    destructor TCollectionItem.Destroy;
    begin
      if FCollection <> nil then
        Release;  // here
      inherited Destroy;
    end;
    
    procedure TCollectionItem.Release;
    begin
      SetCollection(nil); // here
    end;
    
    procedure TCollectionItem.SetCollection(Value: TCollection);
    begin
      if FCollection <> Value then
      begin
        if FCollection <> nil then FCollection.RemoveItem(Self); // here
        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; // here
    end;
  4. Removing item from the collection will run notifiers - including TContosoEditorPropertiesCollection.Update:
    procedure TCollection.Changed;
    begin
      if FUpdateCount = 0 then Update(nil); // here
    end;
    
    procedure TContosoEditorPropertiesCollection.Update(Item: TCollectionItem);
    var
      I: Integer;
    begin
      for I := 0 to Count - 1 do
        GetItem(I).EditContainer.FCellIndex := I;
      Row.EditorsChanged; // here
    end;
  5. The Contoso library is trying to update editors:
    procedure TContosoCustomMultiEditorRow.EditorsChanged;
    begin
      if Properties.Locked or VerticalGrid.IsLoading then Exit;
      ViewInfo.ClearValuesInfo; // here
      Changed;
    end;
    
    procedure TContosoCustomRowViewInfo.ClearValuesInfo;
    begin
      FIsRightToLeftConverted := False;
      FInitialized := False;
      ValuesInfo.Clear; // here
      ValuesLinesInfo.Clear;
    end;
    
    procedure TContosoObjectList.Clear;
    var
      I: Integer;
    begin
      if OwnObjects then
      begin
        for I := 0 to Count - 1 do // = 2
          FreeItem(I); // here
      end;
      inherited Clear;
    end;
  6. There are 2 items in the ValuesInfo list. First one (index 0) is OK, second one (index 1) is what causing the issue:
    destructor TContosoEditCellViewInfo.Destroy;
    begin
      if (EditContainer <> nil) and not EditContainer.IsDestroying then // here
      // ...
    end;
    Here is the problem: EditContainer is actually the Owner, and it points to already deleted object - the one that was deleted on the step 2 inside the TContosoCustomEditorRowProperties.Destroy.
Short recap:
  1. Customer's code deletes an item from the collection;
  2. The item deletes its field;
  3. The collection notifies about item's deletion;
  4. The notification callback tries to clear associated information and accesses the deleted field in process.
So, here is our bug: accessing an already deleted object.

Are you asking how the code was working "flawlessly" before adding EurekaLog to the project? Well, simple: the deleted object remained unchanged in the memory, so the TContosoEditCellViewInfo.Destroy could successfully access the already deleted object and read unchanged data from it. Adding EurekaLog to the project with default settings (enabled memory checks) changes this behaviour by actually erasing deleted object/memory.

So, is it a bug in the Contoso library? While it certainly does look that way, we can not be certain for sure, as we are not experts with that library. So it is also entirely possible that it is a customer's error.

Customer reported that this issue will be taken to the Contoso library support.

P.S. Read more stories like this one or read feedback from our customers.