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