fbclient.dll bug: Stack Overflow while connecting XNET + Windows OS Authentication + Pipe impersonation

4 views
Skip to first unread message

Alexander Liberov

unread,
Jul 6, 2026, 6:23:32 PM (7 hours ago) Jul 6
to firebird-support
FB4 XNET/local attach stack-overflows in fbclient (gds__log recursion) when the calling thread impersonates another user's token

Hello,

We have hit what looks like an fbclient.dll defect on the XNET (local, Protocol=Local)
transport and would appreciate confirmation before we file a tracker issue.

Setup: a Windows service running as LocalSystem authenticates a client over a named
pipe, calls ImpersonateNamedPipeClient() to wear that client's token, and then opens a
Firebird 4 database over XNET with Win_Sspi trusted auth. The attach
(isc_attach_database / FireDAC TFDConnection.Connected := True) throws a Windows stack
overflow (exception C00000FD).

A full minidump shows the overflow is an infinite recursion ENTIRELY inside fbclient.dll,
cycling through gds__log (~5,400 frames). The same attach over TCPIP-to-localhost works
fine, and plain XNET (no impersonation) works fine. It fails only when the thread's
impersonation token differs from the process primary token:

  TCPIP, any identity ....................................... OK
  XNET, no impersonation, any identity (incl. SYSTEM) ....... OK
  XNET, impersonating the SAME user (primary == impersonated) OK
  XNET, LocalSystem impersonating a DIFFERENT user's token .. STACK OVERFLOW

Reproduced on Firebird 4.0.6.3221 and 4.0.7.3271 (x64, Super), fbclient matched to the
server and pinned by absolute path. WireCrypt Required vs Disabled, Providers=Remote,
and thread stacks up to 64 MB make no difference. IpcName=Global\FIREBIRD does not help
(the privilege check that fails runs before the object name is composed). Reverting
impersonation around the attach connects but as the machine account, which defeats
trusted auth.

Likely mechanism (from the v5 source, please correct): the XNET client attach calls
isGlobalKernelPrefix() -> OpenProcessToken/PrivilegeCheck(SeCreateGlobalPrivilege);
under the stripped impersonation token these fail, the failure path calls gds__log,
and gds__log's own path (resolving the log file) fails under the same token and
re-enters logging -> unbounded recursion -> C00000FD.

Questions:
1. Is XNET/local attach supported when the caller impersonates a different user's token
   (the normal pattern for a service doing per-client Win_Sspi auth)? Documented limit,
   or a bug?
2. Is the gds__log self-recursion on the XNET error path a known fbclient defect / fixed
   in a later build?
3. Is there a supported way to make XNET succeed under impersonation while keeping the
   client's OS identity, or is TCPIP-to-localhost the recommended transport here?

I have a ~250-line single-file Delphi/FireDAC console reproduction (no application
framework).  I cannot attach files, so they are at the end of this message.

Please help.

Thanks,
Alex Liberov

=================
program fbmini;

// Minimal reproduction: open a Firebird 4 database over XNET (local shared memory)
// with FireDAC. Nothing else. Built to isolate a stack overflow (Windows exception
// C00000FD) seen only on the XNET/Local attach.
//
// Usage:
//   fbmini.exe <db.fdb>            -> plain open over XNET, then TCPIP (control)
//   fbmini.exe <db.fdb> tcp        -> TCPIP only
//   fbmini.exe <db.fdb> xnet       -> XNET only
//   fbmini.exe <db.fdb> xnetimp    -> XNET open WHILE IMPERSONATING a named-pipe
//                                     client's Windows token (mirrors a service that
//                                     does ImpersonateNamedPipeClient before connect).
//                                     Spawns ITSELF as the client -> impersonated token
//                                     = primary token (same user). Does NOT crash.
//   fbmini.exe <db.fdb> xnetwait   -> like xnetimp but WAITS for an EXTERNAL pipe
//                                     client. Run THIS half as SYSTEM (schtasks/psexec)
//                                     and run --pipeclient from a normal user session:
//                                     primary token (SYSTEM) <> impersonated token
//                                     (the user) -- the combination that stack-overflows.
//                                     Order: TCPIP control first (expect OK), then XNET.
//   fbmini.exe --pipeclient        -> the pipe client half (used by xnetimp internally;
//                                     run manually for xnetwait).
//
// Environment: Delphi (RAD Studio) Win64, FireDAC, Firebird 4.0.7 x64.
//   fbclient.dll pinned via TFDPhysFBDriverLink.VendorLib (absolute path, correct bitness).
//
// Output -> console + fbmini.log beside the exe.

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.StrUtils,
  System.Variants,
  Winapi.Windows,
  FireDAC.Comp.Client,
  FireDAC.Phys.FB,
  FireDAC.Phys.IBBase,
  FireDAC.Stan.Def,
  FireDAC.Stan.Async,
  FireDAC.DApt;

const
  // Known FB 4 x64 client locations, matched to the running server. First hit wins.
  FBCLIENT_CANDIDATES: array[0..1] of string = (
    'C:\Firebird\Firebird_4_0\fbclient.dll',                  // ALEX9 (4.0.6.3221)
    'C:\Program Files\Firebird\Firebird_4_0\fbclient.dll');   // HTRTEST (4.0.7.3271)
  PIPE_NAME = '\\.\pipe\FBMINI_IMP_PROBE';

// SDDL -> SD conversion (not in the stock Delphi Winapi.Windows header).
function ConvertStringSecurityDescriptorToSecurityDescriptorW(
  StringSecurityDescriptor: PWideChar; StringSDRevision: DWORD;
  var SecurityDescriptor: Pointer; SecurityDescriptorSize: PULONG): BOOL; stdcall;
  external 'advapi32.dll';

var
  GLog: TextFile;

function FBClientPath: string;
var S: string;
begin
  for S in FBCLIENT_CANDIDATES do
    if FileExists(S) then Exit(S);
  Result := '';
end;

procedure Say(const S: string);
begin
  WriteLn(S);
  try WriteLn(GLog, S); Flush(GLog); except end;
end;

// Open the DB with the given transport. This is the ENTIRE reproduction:
// create the driver link + connection, set params, set Connected := True.
// ARevert: if True, RevertToSelf immediately before Connected:=True and
// re-impersonate after (workaround test -- attach runs under the primary token).
procedure OpenIt(const ADb: string; AXNet: Boolean; AImpersonating: Boolean = False;
  ARevert: Boolean = False; hPipeForReimp: THandle = 0);
var
  Link: TFDPhysFBDriverLink;
  Con : TFDConnection;
begin
  if AXNet then Say('--- XNET (Protocol=Local, no Server)' +
                    IfThen(ARevert, ' [RevertToSelf around attach]', '') + ' ---')
  else          Say('--- TCPIP (Server=localhost) ---');

  Link := TFDPhysFBDriverLink.Create(nil);
  Con  := TFDConnection.Create(nil);
  try
    // Pin the exact client library (absolute path, correct 64-bit build).
    if FBClientPath <> '' then Link.VendorLib := FBClientPath
    else Say('  (note: no known fbclient.dll found; using auto-detect)');

    Con.DriverName := 'FB';
    Con.LoginPrompt := False;
    Con.Params.Values['Database'] := ADb;

    if AXNet then
    begin
      // XNET / local shared memory: Protocol=Local, and NO Server param
      // (if a Server is present FireDAC downgrades to TCP).
      Con.Params.Values['Protocol'] := 'Local';
    end
    else
    begin
      Con.Params.Values['Server']   := 'localhost';
      Con.Params.Values['Protocol'] := 'TCPIP';
    end;

    // Windows trusted authentication (OS user). This is how the real app connects.
    Con.Params.Values['User_Name'] := '';
    Con.Params.Values['Password']  := '';
    Con.Params.Values['OSAuthent'] := 'Yes';

    try
      if ARevert then RevertToSelf;   // attach under primary token (holds SeCreateGlobalPrivilege)
      try
        Con.Connected := True;   // <-- the only interesting line
        Say('  CONNECTED OK. CURRENT_USER=' +
          VarToStr(Con.ExecSQLScalar('SELECT CAST(CURRENT_USER AS VARCHAR(60)) FROM RDB$DATABASE')));
        Con.Connected := False;
      finally
        // re-impersonate so the caller's finally/RevertToSelf pairing stays balanced
        if ARevert and AImpersonating and (hPipeForReimp <> 0) then
          ImpersonateNamedPipeClient(hPipeForReimp);
      end;
    except
      on E: Exception do
        Say('  FAILED: ' + E.ClassName + ': ' + E.Message);
    end;
  finally
    Con.Free;
    Link.Free;
  end;
end;

// The pipe-client half: connect to the probe pipe so the server can impersonate us.
procedure RunPipeClient;
var h: THandle; wr: DWORD; b: Byte;
begin
  WaitNamedPipe(PIPE_NAME, 5000);
  h := CreateFile(PIPE_NAME, GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0);
  if h = INVALID_HANDLE_VALUE then Exit;
  b := 1; WriteFile(h, b, 1, wr, nil); FlushFileBuffers(h);
  Sleep(20000);          // keep the pipe open while the server impersonates + connects
  CloseHandle(h);
end;

// The server half: create a pipe, spawn ourselves as the client, impersonate the
// client's token, and THEN open the DB over XNET -- exactly the failing pattern.
procedure RunXNetImpersonated(const ADb: string);
var
  hPipe: THandle; sa: SECURITY_ATTRIBUTES; si: STARTUPINFO; pi: PROCESS_INFORMATION;
  cmd: string; connected: BOOL; rd: DWORD; b: Byte; who: array[0..256] of Char; sz: DWORD;
begin
  Say('--- XNET + IMPERSONATED (ImpersonateNamedPipeClient, then Connected:=True) ---');
  FillChar(sa, SizeOf(sa), 0); sa.nLength := SizeOf(sa);
  hPipe := CreateNamedPipe(PIPE_NAME, PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_BYTE or PIPE_READMODE_BYTE or PIPE_WAIT, 1, 4096, 4096, 0, @sa);
  if hPipe = INVALID_HANDLE_VALUE then begin Say('  CreateNamedPipe failed: ' + IntToStr(GetLastError)); Exit; end;

  cmd := '"' + ParamStr(0) + '" --pipeclient';
  FillChar(si, SizeOf(si), 0); si.cb := SizeOf(si); FillChar(pi, SizeOf(pi), 0);
  if not CreateProcess(nil, PChar(cmd), nil, nil, False, CREATE_NO_WINDOW, nil, nil, si, pi) then
    begin Say('  CreateProcess(client) failed: ' + IntToStr(GetLastError)); Exit; end;

  connected := ConnectNamedPipe(hPipe, nil);
  if (not connected) and (GetLastError <> ERROR_PIPE_CONNECTED) then
    begin Say('  ConnectNamedPipe failed: ' + IntToStr(GetLastError)); Exit; end;
  ReadFile(hPipe, b, 1, rd, nil);

  sz := Length(who); if GetUserName(who, sz) then Say('  primary token user = ' + who);
  if not ImpersonateNamedPipeClient(hPipe) then
    begin Say('  ImpersonateNamedPipeClient failed: ' + IntToStr(GetLastError)); Exit; end;
  sz := Length(who); if GetUserName(who, sz) then Say('  impersonated token user = ' + who);
  try
    OpenIt(ADb, True);   // <-- XNET open while impersonating. Overflows here.
  finally
    RevertToSelf;
  end;
  CloseHandle(hPipe);
  WaitForSingleObject(pi.hProcess, 5000);
  CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
end;

// The standalone crash mode: create a pipe anyone can open, WAIT for an external
// client (started manually in a different user's session), impersonate its token,
// then attach. Run this half as SYSTEM so primary token <> impersonated token.
// AStep selects WHICH attach to attempt (each run is separate because the crash
// terminates the process):
//   'tcp'    -> TCPIP control while impersonated        (expected: CONNECTED OK)
//   'xnet'   -> plain XNET while impersonated           (expected: C00000FD overflow)
//   'revert' -> XNET with RevertToSelf around attach    (workaround test)
//   'all'    -> tcp then xnet (default; stops at the overflow)
procedure RunXNetWaitExternal(const ADb, AStep: string);
var
  hPipe: THandle; sa: SECURITY_ATTRIBUTES; pSD: Pointer;
  connected: BOOL; rd: DWORD; b: Byte; who: array[0..256] of Char; sz: DWORD;
begin
  Say('--- XNET + IMPERSONATED EXTERNAL CLIENT  step=' + AStep +
      '  (waiting on ' + PIPE_NAME + ') ---');
  // Everyone:GenericAll DACL so a normal user's session can open a SYSTEM-created pipe.
  pSD := nil;
  if not ConvertStringSecurityDescriptorToSecurityDescriptorW('D:(A;;GA;;;WD)', 1, pSD, nil) then
    begin Say('  SDDL conversion failed: ' + IntToStr(GetLastError)); Exit; end;
  FillChar(sa, SizeOf(sa), 0); sa.nLength := SizeOf(sa); sa.lpSecurityDescriptor := pSD;
  hPipe := CreateNamedPipe(PIPE_NAME, PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_BYTE or PIPE_READMODE_BYTE or PIPE_WAIT, 1, 4096, 4096, 0, @sa);
  if hPipe = INVALID_HANDLE_VALUE then begin Say('  CreateNamedPipe failed: ' + IntToStr(GetLastError)); Exit; end;

  Say('  waiting for external --pipeclient (no timeout; kill me if forgotten) ...');
  connected := ConnectNamedPipe(hPipe, nil);
  if (not connected) and (GetLastError <> ERROR_PIPE_CONNECTED) then
    begin Say('  ConnectNamedPipe failed: ' + IntToStr(GetLastError)); Exit; end;
  ReadFile(hPipe, b, 1, rd, nil);

  sz := Length(who); if GetUserName(who, sz) then Say('  primary token user      = ' + who);
  if not ImpersonateNamedPipeClient(hPipe) then
    begin Say('  ImpersonateNamedPipeClient failed: ' + IntToStr(GetLastError)); Exit; end;
  sz := Length(who); if GetUserName(who, sz) then Say('  impersonated token user = ' + who);
  try
    if (AStep = 'tcp') then
      OpenIt(ADb, False, True)                       // TCPIP control  -> expect OK
    else if (AStep = 'xnet') then
      OpenIt(ADb, True, True)                        // plain XNET     -> expect overflow
    else if (AStep = 'revert') then
      OpenIt(ADb, True, True, True, hPipe)           // XNET+RevertToSelf -> workaround
    else
    begin                                            // 'all' (default)
      OpenIt(ADb, False, True);                      // TCPIP control
      OpenIt(ADb, True, True);                       // XNET (stops here on overflow)
    end;
  finally
    RevertToSelf;
  end;
  CloseHandle(hPipe);
end;

var
  Db, Mode: string;
begin
  if (ParamCount >= 1) and SameText(ParamStr(1), '--pipeclient') then
  begin
    RunPipeClient;
    Halt(0);
  end;
  if ParamCount < 1 then
  begin
    WriteLn('usage: fbmini.exe <db.fdb> [xnet|tcp|xnetimp]');
    Halt(1);
  end;
  Db := ParamStr(1);
  Mode := LowerCase(ParamStr(2));

  AssignFile(GLog, ExtractFilePath(ParamStr(0)) + 'fbmini_' +
    IfThen(Mode = '', 'default', Mode) + IfThen(ParamStr(3) <> '', '_' + LowerCase(ParamStr(3)), '') + '.log');
  try Rewrite(GLog); except end;
  Say('fbmini on ' + GetEnvironmentVariable('COMPUTERNAME') + '  DB=' + Db);
  Say('fbclient pinned = ' + FBClientPath);

  if Mode = 'tcp' then
    OpenIt(Db, False)
  else if Mode = 'xnet' then
    OpenIt(Db, True)
  else if Mode = 'xnetimp' then
    RunXNetImpersonated(Db)
  else if Mode = 'xnetwait' then
    RunXNetWaitExternal(Db, LowerCase(ParamStr(3)))
  else
  begin
    OpenIt(Db, False);   // TCPIP control first
    OpenIt(Db, True);    // then XNET
  end;

  Say('DONE.');
  try CloseFile(GLog); except end;
end.
=================

fbmini.dpr
FORUM-POST.md
README.txt
Reply all
Reply to author
Forward
0 new messages