【问题标题】:Access Violation When Using Indy OpenSSL and MySql使用 Indy OpenSSL 和 MySql 时的访问冲突
【发布时间】:2019-07-16 13:29:49
【问题描述】:

使用 MySql 8.0.16、Delphi 10.3 Rio 以及随附的标准版 Indy。

我正在使用TIdServerIOHandlerSSLOpenSSL 的实例和TIdHttpServer 的实例,使用从Fulgan 下载的OpenSSL 1.0.2s。我所有的 Indy 组件都是在运行时在代码中创建的。

在我关闭应用程序并在IdSSLOpenSSLHeaders.Unload() 中获得访问冲突之前,一切似乎都正常工作,该访问冲突是从IdSSLOpenSSL.pas 文件的finalization 部分调用的。

项目引发异常类 $C0000005 并带有消息“c0000005 ACCESS_VIOLATION”

堆栈跟踪如下:

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

崩溃就在这里:

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

我目前正在释放TIdHTTPServer,然后是IOHandler

当我连接到 MySql 数据库时出现问题。看起来 libmysql 也将错误队列用于主线程,并且还通过调用 ERR_remove_thread_state() 来释放队列。重现的最少代码在这里:

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.

【问题讨论】:

  • ERR_remove_thread_state 是一个函数指针。我不认为他们是这样被释放的。此外,在此调用之前对 open ssl 函数指针的调用似乎有效。
  • 这种情况可能发生,如果你明确释放了后来被其所有者释放的东西,或者试图释放已经被其所有者释放的东西。如果您跳入ERR_remove_thread_state 会发生什么?你最终会在哪里?
  • 更新了堆栈跟踪信息。 ERR_remove_thread_state 似乎是其中一个openssl dll(确切地说是libeay32.dll)中的一个函数。此外,IO 处理程序是使用 nil 所有者创建的(所以我明确地释放它)。然而,当应用程序关闭时,所有组件都被释放后,错误就会发生。
  • 在所有组件都被释放后发生错误意味着你正在不恰当地释放一些其他代码试图作为清理的一部分而释放的东西。如果您希望我们帮助您确定可能是什么,您需要发布一个 minimal reproducible example 来演示该问题。你能通过一个只包含你的组件创建和清理代码的简单测试应用程序来重现它吗?

标签: delphi indy


【解决方案1】:

造成这种情况的原因是单元的最终确定部分的运行顺序。这是由单位出现在uses 子句中的顺序决定的。初始化部分按照它们在使用中出现的顺序运行。终结部分按反向顺序运行。

按此顺序,IdSSLOpenSSL.pas 的 finalize 部分将在 libmysql.dll 被 FireDAC 卸载后运行,并且在 Indy 尝试清理和卸载 OpenSSL 时会导致 AcessViolation:

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;

按此顺序,IdSSLOpenSSL.pas 的 finalize 部分将在 libmysql.dll 被 FireDAC 卸载之前运行,并且不会出现错误:

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;

【讨论】:

  • 更改uses 顺序,在Indy 已经调用它之后不会让MySql 调用ERR_remove_thread_state() 只会产生相同的崩溃,而是在libmysql 内部?每个线程多次调用ERR_remove_thread_state() 是否安全?
  • 它可能会在 libmysql 中抛出异常,但它可能会处理它,因为应用程序本身没有出现异常。或者可能有一种方法可以在调用它之前检查线程错误队列状态。我不是 openssl 专家,所以我不确定。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-06
  • 1970-01-01
  • 2010-11-19
  • 2016-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多