Nice bug that took me a lot of time to figure out :)

Nice bug that took me a lot of time to figure out :)

type
  TTest = class
    s: string;
    constructor Create(s: string);
  end;

constructor TTest.Create(s: string);
begin
  Self.s := s;
end;

var
  t: TTest;

procedure NiceBug(const s: string);
begin
// here RefCount(s) = 1 because of "const" keyword
  t.Free;
// RefCount(s) = 0 !

// at this point s is no more allocated  and should not be used anymore !
  ShowMessage(s);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  t := TTest.Create('Hello');
  NiceBug(t.s);
end;

Comments

  1. I bet FastMM fulldebug would have pointed you there pretty quick.

    ReplyDelete
  2. Well, Delphi pointed my an error in _UStrClr() but it's not easy to track in a real project. the string came from a object that request its own deletion by UID and the error occurs far away while building a JSON notification thrown over the network :/ And all this because the string is put in a Record that automatically tries to release this already released string.

    Note that in the sample code above, there's no error at all, the string is deallocated but still in memory.

    ReplyDelete
  3. I looked into it and FastMM does not do what I thought. However I think you can hook UClrStr to mark the memory to easier spot strings that point to freed memory (just quick and dirty and tested in XE only):

    unit UseStringAfterClearDetector;

    interface

    implementation

    uses
      DDetours,
      Windows;

    type
      PStrRec = ^StrRec;
      StrRec = packed record
        codePage: Word;
        elemSize: Word;
        refCnt: Longint;
        length: Longint;
      end;

    procedure MarkFreed(P: PStrRec);
    var
      i: Integer;
    begin
      for i := 0 to P.length - 1 do
        PChar(PByte(P) + SizeOf(StrRec))[i] := 'X';
    end;

    function _UStrClr(var S): Pointer;
    var
      P: PStrRec;
    begin
      if Pointer(S) <> nil then
      begin
        P := Pointer(NativeInt(S) - SizeOf(StrRec));
        Pointer(S) := nil;
        if P.refCnt > 0 then
        begin
          if InterlockedDecrement(P.refCnt) = 0 then
          begin
            MarkFreed(P);
            FreeMem(P);
          end;
        end;
      end;
      Result :=@S;
    end;

    var
      p: Pointer;

    function GetUStrClrAddr: Pointer;
    asm
      mov eax,offset System.@UStrClr
    end;

    initialization
      p := InterceptCreate(GetUStrClrAddr,@_UStrClr);

    finalization
      InterceptRemove(p);

    end.

    ReplyDelete
  4. yes but I already knows that it was freed, the question was how ? I don't know if FastMM is able to tell me that "s" was a member of an instance of TTest that was freed ;)

    ReplyDelete

Post a Comment