2

In old Delphi applications which use the old and deprecated but still used BDE database engine with Paradox database files residing on a Windows 10 computer that's updated to the 1803 "Spring Creators Update" version, but the client computers using any older version of Windows like Windows 10 1709 or Windows 7, opening a Paradox table sometimes fails with a "No more files" error, idapi32.dll error code DBIERR_OSENMFILE. This raises a EDBEngineError exception in DBTables.pas / TTable.GetHandle(), which is called by TTable.CreateHandle, called by TBDEDataSet.OpenCursor().

The error seems to be caused by some file-sharing related changes in the Windows 10 1803 update. Removing the 1803 update from the file-sharing Windows 10 computer, or updating all the client computers to Windows 10 + 1803 seems to make the error go away.

People have speculated that the changes have something to do with the SMB protocol, maybe Windows Defender and/or other security related issues. Here's a Google Plus discussion https://plus.google.com/106831056534874810288/posts/F4nsoTz2pDi

How could the "No more files" error be worked around by some reasonably easily doable changes in the Delphi application, while allowing the file-sharing client and server computers to keep using heterogeneous Windows versions?

Please try to refrain from answering or commenting self-evident things like "the sky is blue" or "BDE is old and deprecated". Keeping BDE is a decision that cannot be changed, certainly not as a "bug fix".

As an emergency fix, we have resorted to simply re-trying DbiOpenTable, when it returns the DBIERR_OSENMFILE error code. I posted an answer with source code to the idapi32.dll hack. So far it seems that if the first DbiOpenTable says "No more files", the second try succeeds, and the application works without noticing anything.

Side S. Fresh
  • 3,015
  • 2
  • 16
  • 18
  • 2
    The 1803 update disables SMBv1. You can renenable this via windows features: https://www.tenforums.com/tutorials/107605-enable-disable-smb1-file-sharing-protocol-windows.htmlsetting. – whosrdaddy Jun 07 '18 at 13:59
  • Obligatory (despite your request) comment: The BDE has been deprecated for more than a decade now. That's sufficient time to have found a replacement. At some point you're going to have to do so, because it becomes harder and harder to make it work, or simply decide to stop updating other software and the OS where the BDS is being used, and you have to expect others to stop supporting your use of that antiquated platform as well. Seriously - more than a decade. – Ken White Jun 07 '18 at 20:00
  • @whosrdaddy: regardless of whether SMB1 support has anything to do with the intermittent "No more files" errors, enabling SMB v1 cannot be suggested as a "bug fix", for security related reasons listed on the page you linked. – Side S. Fresh Jun 08 '18 at 07:36

2 Answers2

2
  • WARNING: what follows is a hack. A kludge. Band-aid, glue, duct tape and chewing gum. BDE is old. You are completely on your own if you use BDE and/or if you try this hack. I accept no responsibility over its use. If it works for you, good for you. If it ruins your business, bad for you.

Since the Paradox tables still mostly worked and the error seemed to be slightly randomly triggered, and since someone suspected Windows Defender having something to do with it, I thought maybe it just needs some kicking around. If DbiOpenTable() suddenly starts sometimes failing over a certain combination of SMB client/server versions, because "No more files" ... then why not just try the file operation again. I put an "if it returns a DBIERR_OSENMFILE error, then Sleep() and try again" logic around the DbiOpenTable function, and guess what - it seemed to work.

Hacking around the BDE's "features" is familiar to anyone who has to maintain BDE based applications. So I made a patching hook around idapi32.dll's DbiOpenTable function, starting from an old routine written by Reinaldo Yañez originally to fix the "insufficient disk space" error with BDE when the free disk space is at a 4 GB boundary. See https://cc.embarcadero.com/Item/21475

To use this, add Fix1803 in a uses clause, and call PatchBDE somewhere before starting to open Paradox tables. Maybe call UnPatchBDE when you're done, though I don't think that's necessary.

But remember, you're on your own, and this is highly experimental code.

unit Fix1803;
// * KLUDGE WARNING * 
// Patch (hack) idapi32.dll DbiOpenTable() to try harder, to work with Windows 10 1803 "Spring Creators Update".
//
// The patching routine is an extension of code originally written by Reinaldo Yañez.
//  see https://cc.embarcadero.com/Item/21475
//
// Some original Spanish comments are left in place.

interface

procedure PatchBDE;
procedure UnPatchBDE;

implementation

uses
  Windows, Db, DbTables, BDE, SysUtils;

// -------------------------------------------  DbiOpenTable hook
var DbiOpenTable_address_plus_9 : Pointer;
function Actual_DbiOpenTable_CallStub(hDb: hDBIDb; pszTableName: PChar; pszDriverType: PChar; pszIndexName: PChar; pszIndexTagName: PChar; iIndexId: Word; eOpenMode: DBIOpenMode; eShareMode: DBIShareMode; exltMode: XLTMode; bUniDirectional: Bool; pOptParams: Pointer; var hCursor: hDBICur): DBIResult stdcall; assembler;
asm
// these two instructions are implicitly contained in the start of the function
//        push ebp
//        mov ebp, esp
        add esp, $fffffee8
        jmp  dword ptr [DbiOpenTable_address_plus_9]
end;

function LogHook_DbiOpenTable (hDb: hDBIDb; pszTableName: PChar; pszDriverType: PChar; pszIndexName: PChar; pszIndexTagName: PChar; iIndexId: Word; eOpenMode: DBIOpenMode; eShareMode: DBIShareMode; exltMode: XLTMode; bUniDirectional: Bool; pOptParams: Pointer; var hCursor: hDBICur): DBIResult stdcall;
var
  i : Integer;
begin
  Result := Actual_DbiOpenTable_CallStub(hDb, pszTableName, pszDriverType, pszIndexName, pszIndexTagName, iIndexId, eOpenMode, eShareMode, exltMode, bUniDirectional, pOptParams, hCursor);
  // if we got the "No more files" error, try again... and again.
  i := 1;
  while (Result = DBIERR_OSENMFILE) and (i < 10) do
  begin
    Windows.Sleep(i);
    Result := Actual_DbiOpenTable_CallStub(hDb, pszTableName, pszDriverType, pszIndexName, pszIndexTagName, iIndexId, eOpenMode, eShareMode, exltMode, bUniDirectional, pOptParams, hCursor);
    Inc(i);
  end;
end;

// -------------------------------------------  Patching routines
const // The size of the jump instruction written over the start of the original routine is 5 bytes
  NUM_BYTES_OVERWRITTEN_BY_THE_PATCH = 5;

type
  TRYPatch = record
    OrgAddr: Pointer;
    OrgBytes: array[0..NUM_BYTES_OVERWRITTEN_BY_THE_PATCH-1] of Byte;
  end;

procedure TRYPatch_Clear(var ARYPatch : TRYPatch);
begin
  FillChar(ARYPatch, SizeOf(TRYPatch), 0);
end;

function RedirectFunction(OldPtr, NewPtr, CallOrigStub : Pointer; var OriginalRoutineAddressPlusN: Pointer; NumBytesInCompleteInstructionsOverwritten : Integer): TRYPatch;
type
  PPtr=^pointer;
  PPPtr=^PPtr;
  TByteArray=array[0..maxint-1] of byte;
  PByteArray=^TByteArray;

function SameBytes(Ptr1, Ptr2 : Pointer; NumBytes : Integer) : Boolean;
  var
    i : Integer;
  begin
    Result := true;
    i := 0;
    while (Result) and (i < NumBytes) do
    begin
      Result := Result and ((PByteArray(Ptr1)^[i] = PByteArray(Ptr2)^[i]));
      Inc(i);
    end;
  end;

var
  PatchingAddress : Pointer;
  OldProtect,
  Protect   : DWORD;
  p: PByteArray;
  i : Integer;
begin
  PatchingAddress := OldPtr;
  if PWord(PatchingAddress)^ = $25FF then
  begin {Es un JMP DWORD PTR [XXXXXXX](=> Esta utilizando Packages)}
    p := PatchingAddress;
    PatchingAddress := (PPPtr(@p[2])^)^; // PatchingAddress now points to the start of the actual original routine
  end;


// Safety check (as if this thing was "safe"). The given replacement routine must start with the same bytes as the replaced routine.
  // Otherwise something is wrong, maybe a different version of idapi32.dll or something.
  if (CallOrigStub <> nil) and not SameBytes(PatchingAddress, CallOrigStub, NumBytesInCompleteInstructionsOverwritten) then
    raise Exception.Create('Will not redirect function, original call stub doesn''t match.');


// Change memory access protection settings, so we can change the contents
  VirtualProtect(PatchingAddress, NUM_BYTES_OVERWRITTEN_BY_THE_PATCH, PAGE_READWRITE, @OldProtect);


// Save the old contents of the first N bytes of the routine we're hooking
  Result.OrgAddr := PatchingAddress; // Save the address of the code we're patching (which might not be the same as the original OldPtr given as parameter)
  for i := 0 to NUM_BYTES_OVERWRITTEN_BY_THE_PATCH-1 do
    result.OrgBytes[i] := PByte(Integer(PatchingAddress) + i)^;


// Replace the first bytes of the original function with a relative jump to the new replacement hook function
  // First write the instruction opcode, $E9 : JMP rel32
  PByte(PatchingAddress)^:= $E9;
  // Then write the instruction's operand: the relative address of the new function 
  PInteger(Integer(PatchingAddress)+1)^ := Integer(NewPtr) - Integer(PatchingAddress) - 5;


// Address to jump to, for the replacement routine's jump instruction 
  OriginalRoutineAddressPlusN := Pointer(Integer(PatchingAddress) + NumBytesInCompleteInstructionsOverwritten);


// Restore the access protection settings
  VirtualProtect(PatchingAddress, NUM_BYTES_OVERWRITTEN_BY_THE_PATCH, OldProtect, @Protect);
  FlushInstructionCache(GetCurrentProcess, PatchingAddress, NUM_BYTES_OVERWRITTEN_BY_THE_PATCH);
end;


procedure RestorePatch(RestorePatch: TRYPatch);
var
  OldProtect,
  Protect   : DWORD;
  OldPtr: Pointer;
  i : Integer;
begin
  OldPtr := RestorePatch.OrgAddr;
  VirtualProtect(OldPtr, NUM_BYTES_OVERWRITTEN_BY_THE_PATCH, PAGE_READWRITE, @OldProtect);
  for i := 0 to NUM_BYTES_OVERWRITTEN_BY_THE_PATCH-1 do
    PByte(Integer(OldPtr) + i)^ := RestorePatch.OrgBytes[i];

    VirtualProtect(OldPtr, NUM_BYTES_OVERWRITTEN_BY_THE_PATCH, OldProtect, @Protect);
  FlushInstructionCache(GetCurrentProcess, OldPtr, NUM_BYTES_OVERWRITTEN_BY_THE_PATCH);
end;


var
  idapi32_handle: HMODULE;
  Patch_DbiOpenTable : TRYPatch;


procedure PatchBDE;
begin
  if idapi32_handle <> 0 then Exit; // already_patched
  idapi32_handle := LoadLibrary('idapi32');
  if idapi32_handle <> 0 then
  begin
    Patch_DbiOpenTable := RedirectFunction(GetProcAddress(idapi32_handle, 'DbiOpenTable'), @LogHook_DbiOpenTable, @Actual_DbiOpenTable_CallStub, DbiOpenTable_address_plus_9, 9);
  end;
end;

procedure UnPatchBDE;
begin
  if idapi32_handle <> 0 then
  begin
    {Leave everything as before, just in case...}
    if Patch_DbiOpenTable.OrgAddr <> nil then
      RestorePatch(Patch_DbiOpenTable);
    FreeLibrary(idapi32_handle);
    idapi32_handle := 0;
  end;
end;

initialization
  idapi32_handle := 0;
  TRYPatch_Clear(Patch_DbiOpenTable); 

end.
Rudy Velthuis
  • 28,387
  • 5
  • 46
  • 94
Side S. Fresh
  • 3,015
  • 2
  • 16
  • 18
-2

VMWare, Virtual Box, etc to virtualize an Windows 7. If, as you say, W7 work flawlessly that would solve the problem.

Fabricio Araujo
  • 3,810
  • 3
  • 28
  • 43