Search code examples
delphiindy

Access Violation When Using Indy OpenSSL and MySql


Using MySql 8.0.16, Delphi 10.3 Rio, and the standard version of Indy that comes with it.

I'm using an instance of TIdServerIOHandlerSSLOpenSSL with an instance of TIdHttpServer, using OpenSSL 1.0.2s downloaded from Fulgan. All of my Indy components are created in code at runtime.

Everything appears to work until I close the app and get an Access Violation in IdSSLOpenSSLHeaders.Unload() which gets called from the finalization section of the IdSSLOpenSSL.pas file.

Project rasied exception class $C0000005 with message 'c0000005 ACCESS_VIOLATION'

The stack trace is as follows:

IdSSLOpenSSLHeaders.Unload
IdSSLOpenSSL.UnloadOpenSSLLibrary
IdSSLOpenSSL.Finalization
System.FinalizeUnits
System._Halt()
MayApp.MayApp
:0000000076DC556D; C:\Windows\system32\kernel.dll
:0000000076F2385D; ntdll.dll

The crash is here:

if Assigned(ERR_remove_thread_state) then begin
  ERR_remove_thread_state(nil); <-- Access Violation here
end

I am currently freeing the TIdHTTPServer First, then the IOHandler.

The problem occurs when I connect to a MySql database. It looks like libmysql also uses the error queue for the main thread and it also frees the queue by calling ERR_remove_thread_state(). The minimum code to reproduce is here:

program OpenSSLIssue;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.Classes, System.SysUtils, System.IoUtils, System.JSON, WinApi.Windows,
  WinApi.Messages, System.Generics.Collections, IdServerIOHandler, IdSSL, IdGlobal,
  IdSSLOpenSSL, IdBaseComponent, IdComponent, IdCustomTCPServer, IdTCPServer,
  IdUDPBase, IdUDPServer,IdSocketHandle, IdCustomHTTPServer, IdHTTPServer, IdContext,
  IdCoderMIME, IdSSLOpenSSLHeaders, FireDac.Comp.Client, FireDac.Phys.MySQL,
  FireDAC.Stan.Def;

type
  TEndPoint = class
  protected
    { Protected declarations }
    FIP: String;
    FPort: WORD;
    FProtocol: String;
    FServer: TIdHttpServer;
    FIOHandler: TIdServerIOHandlerSSLOpenSSL;
    procedure QuerySSLPort(APort: Word; var AUseSSL: Boolean);
    function SSLVerifyPeer(Certificate: TIdX509; AOk: Boolean; ADepth,  AError: Integer): Boolean;
  public
    { Public declarations }
    constructor Create(AIP: String; APort: WORD; AProtocol: String);
    destructor Destroy; override;
    function Start: Boolean;
    procedure Stop;
  end;

constructor TEndPoint.Create(AIP: String; APort: WORD; AProtocol: String);
begin
  var LPath := ExcludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
  IdOpenSSLSetLibPath(LPath);

  FIP := AIP;
  FPort := APort;
  FProtocol := AProtocol.ToUpper;

  FServer := TIdHttpServer.Create(nil);
  FServer.DefaultPort := APort;
  FServer.OnQuerySSLPort := QuerySSLPort;

  if 'HTTPS' = FProtocol then
  begin
    FIOHandler := TIdServerIOHandlerSSLOpenSSL.Create(nil);
    FIOHandler.SSLOptions.SSLVersions := [sslvTLSv1_2];
    FIOHandler.SSLOptions.Method := sslvTLSv1_2;

    FIOHandler.SSLOptions.CertFile := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+ 'device.crt';
    FIOHandler.SSLOptions.KeyFile := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+ 'myDevice.key';
    FIOHandler.SSLOptions.RootCertFile := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+ 'myRootCA.pem';
    FIOHandler.OnVerifyPeer := SSLVerifyPeer;
    FServer.IOHandler := FIOHandler;
  end;

  var LBinding := FServer.Bindings.Add;
  LBinding.IP := AIP;
  LBinding.Port := APort;
end;

destructor TEndPoint.Destroy;
begin
  FServer.Free;
  if nil <> FIOHandler then
    FIOHandler.Free;
  inherited Destroy;
end;

procedure TEndPoint.QuerySSLPort(APort: Word; var AUseSSL: Boolean);
begin
  AUseSSL := 'HTTPS' = FProtocol;
end;

function TEndPoint.SSLVerifyPeer(Certificate: TIdX509; AOk: Boolean; ADepth,  AError: Integer): Boolean;
begin
  Result := AOK;
end;

function TEndPoint.Start: Boolean;
begin
  Result := FALSE;
  try
    FServer.Active := TRUE;
    Result := TRUE;
  except
  end;
end;

procedure TEndPoint.Stop;
begin
  try
    FServer.Active := FALSE;
  except
    //Suppress any exceptions as sockets are closed off
  end;
end;

function GetConnection(ADatabaseName, AUserName, APAssword, ADatabase, AHost: String): TFDConnection;
begin
  var LConnectionDef := FDManager.ConnectionDefs.FindConnectionDef(ADatabaseName + '_Connection');
  if nil = LConnectionDef then
  begin
    var LParams := TStringList.Create;
    LParams.Add('User_Name=' + AUserName);
    LParams.Add('Password=' + APassword);
    LParams.Add('Server=' + AHost);
    LParams.Add('Database=' + ADatabase);
    FDManager.AddConnectionDef(ADatabaseName + '_Connection', 'MYSQL', LParams);
  end else
  begin
    var LIndex := LConnectionDef.Params.IndexOfName('Server');
    LConnectionDef.Params[LIndex] := AHost;
    LConnectionDef.Params.UserName := AUserName;
    LConnectionDef.Params.Password := APassword;
    LConnectionDef.Params.Database := ADatabase;
  end;

  Result := TFDConnection.Create(nil);
  Result.LoginPrompt := FALSE;
  Result.DriverName := 'MYSQL';
  Result.ConnectionDefName := ADatabaseName + '_Connection';
end;

(* Create the DQL in MySql Workbeanch with the following:

CREATE DATABASE IF NOT EXISTS `MyTestDB`;

USE MyTestDB;


CREATE TABLE IF NOT EXISTS `TestTable`(
    `VersionID` int NOT NULL,
    `VerMajor` int NOT NULL,
    `VerMinor` int NOT NULL,
    `VerRelease` int NOT NULL,
    PRIMARY KEY (`VersionID`)
);

*)
begin
  var DriverLink := TFDPhysMYSQLDriverLink.Create(nil);
  DriverLink.VendorLib := String.Format('%s\libmysql.dll',[ExcludeTrailingPathDelimiter(ExtractFileDir( ParamStr(0) ))]);

  try
    var FEndpoint := TEndPoint.Create('127.0.0.1', 8200, 'https');
    try
      FEndpoint.Start;

      var LConn := GetConnection('MyTestDB', 'root', 'rootPasswd', 'MyTestDB', 'localhost');
      try
        LConn.Open;
        WriteLn('Connection Open');
        Sleep(1000);
        LConn.Close;
      finally
        LConn.Free;
      end;
      FEndpoint.Stop;
    finally
      FEndpoint.Free;
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

  DriverLink.Free;

end.

Solution

  • The cause of this is the order in which the finalization sections of the units are run. This is dictated by the order in which the Units appear in the uses clause. The initialization sections are run in the order they appear in the uses. The finalization sections are run in reverse order.

    In this order the finalize section of IdSSLOpenSSL.pas will be run after libmysql.dll is unloaded by FireDAC and will result in an AcessViolation when Indy attempts to cleanup and unload OpenSSL:

    uses
      System.Classes, System.SysUtils, System.IoUtils, System.JSON, WinApi.Windows,
      WinApi.Messages, System.Generics.Collections, FireDAC.Stan.Def, FireDac.Phys.MySQL,
      IdServerIOHandler, IdSSL, IdGlobal, IdBaseComponent, IdComponent, IdCustomTCPServer,
      IdTCPServer, IdUDPBase, IdUDPServer,IdSocketHandle, IdCustomHTTPServer, IdHTTPServer,
      IdContext, IdCoderMIME, IdSSLOpenSSLHeaders,
    
      //finlaize section of IdSSLOpenSSL will be run after 
      //libmysql.dll is unloaded byFireDAC
    
      IdSSLOpenSSL,
      FireDac.Comp.Client;
    

    In this order the finalize section of IdSSLOpenSSL.pas will be run before libmysql.dll is unloaded by FireDAC and there will be no errors:

    uses
      System.Classes, System.SysUtils, System.IoUtils, System.JSON, WinApi.Windows,
      WinApi.Messages, System.Generics.Collections, FireDAC.Stan.Def, FireDac.Phys.MySQL,
      IdServerIOHandler, IdSSL, IdGlobal, IdBaseComponent, IdComponent, IdCustomTCPServer,
      IdTCPServer, IdUDPBase, IdUDPServer,IdSocketHandle, IdCustomHTTPServer, IdHTTPServer,
      IdContext, IdCoderMIME, IdSSLOpenSSLHeaders,
    
      //finlaize section of IdSSLOpenSSL will be run before 
      //libmysql.dll is unloaded byFireDAC
    
      FireDac.Comp.Client,
      IdSSLOpenSSL;