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.
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;