Search code examples
c#delphidelphi-6

VirtualProtect - AV : Attempted to read or write protected memory


I have Delphi 6 DLL which internally uses a third party library to do the automatic resource string translation.

In the library source code 2 RTL functions (ShortCutToText and LoadResString) are replaced with the multilingual counterparts (trShortCutToText and trLoadResString). Code is shown below:

VirtualProtect(@ShortCutToText, SIZE_C, PAGE_READWRITE, @protectType);
Move((@ShortCutToText)^, shortCutBuffer, SIZE_C);
Move((@trShortCutToText)^, (@ShortCutToText)^, SIZE_C);
VirtualProtect(@ShortCutToText, SIZE_C, protectType, @protectType);
FlushInstructionCache(GetCurrentProcess, @ShortCutToText, SIZE_C);

and

VirtualProtect(@LoadResString, SIZE_C, PAGE_READWRITE, @protectType);
Move((@LoadResString)^, loadResStringBuffer, SIZE_C);
Move((@trLoadResString)^, (@LoadResString)^, SIZE_C);
VirtualProtect(@LoadResString, SIZE_C, protectType, @protectType); //<--Exception thrown at this line
FlushInstructionCache(GetCurrentProcess, @LoadResString, SIZE_C);

Now when I call functions of this DLL from within another Delphi 6 application everything works fine.

However when I call the DLL from a C# application (compiled as 32bit app on 64bit machine), my c# application simply crashes.

On debugging the DLL, I found that the first block of the above 3rd party code executes without any issue however the second block fails at second last line.

The error message is shown below:

System.AccessViolationException was unhandled
  Message="Attempted to read or write protected memory. This is often an indication that other memory is corrupt."
  Source="TestWinform"
  StackTrace:
       at TestWinform.TestSwitching()
       at TestWinform.Form1.button1_Click(Object sender, EventArgs e)
       at System.Windows.Forms.Control.OnClick(EventArgs e)
       at System.Windows.Forms.Button.OnClick(EventArgs e)
       at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
       at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
       at System.Windows.Forms.Control.WndProc(Message& m)
       at System.Windows.Forms.ButtonBase.WndProc(Message& m)
       at System.Windows.Forms.Button.WndProc(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
       at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
       at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
       at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.Run(Form mainForm)
       at TestWinform.Program.Main()
       at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException:

EDIT:

I have created a sample DLL project in Delphi 6 to replicate the problem.

Source code from DelphiDllTest.dpr file is given below:

library DelphiDllTest;
uses
  SysUtils,
  Classes,
  MyUnit in 'MyUnit.pas';

{$R *.res}

procedure TestSwitching; export;
var
  myDict : TMyDictionary;
begin
  myDict := TMyDictionary.Create(nil);
  try        
    myDict.Open;
  finally
    myDict.Close;
    FreeAndNil(myDict);
  end;
end;

exports
TestSwitching;
begin
end.

Source of MyUnit.pas is given below:

    unit MyUnit;

    interface

    uses
      Classes, Windows;

    type

     TMyDictionary = class(TComponent)
      public
        constructor Create(owner: TComponent); override;
        destructor Destroy; override;
        procedure Open; virtual;
        procedure Close; virtual;
     end;

    function TranslateLoadResString(resStringRec: PResStringRec): String;
    function trLoadResString(resStringRec: PResStringRec): String;

    const
      SIZE_C = 32;

    var
      loadResStringBuffer: array[0..SIZE_C] of Byte;

    implementation

    constructor TMyDictionary.Create(owner: TComponent);
    begin
      inherited Create(owner);
    end;

    destructor TMyDictionary.Destroy;
    begin
      inherited Destroy;
    end;

    procedure TMyDictionary.Open;
    var
      protect: Integer;
    begin
      VirtualProtect(@LoadResString, SIZE_C, PAGE_READWRITE, @protect);
      Move((@LoadResString)^, loadResStringBuffer, SIZE_C);
      Move((@trLoadResString)^, (@LoadResString)^, SIZE_C);
      VirtualProtect(@LoadResString, SIZE_C, protect, @protect);
      FlushInstructionCache(GetCurrentProcess, @LoadResString, SIZE_C);
    end;

    procedure TMyDictionary.Close;
    var
      protect: Integer;
    begin
        VirtualProtect(@LoadResString, SIZE_C, PAGE_READWRITE, @protect);
        Move(loadResStringBuffer, (@LoadResString)^, SIZE_C);
        VirtualProtect(@LoadResString, SIZE_C, protect, @protect);
    end;


    function TranslateLoadResString(resStringRec: PResStringRec): String;
    var
      buffer: array[0..1023] of Char;
    begin
      try
        if resStringRec <> nil then
        begin
          if resStringRec.Identifier < 64*1024 then
          begin
            SetString(
              Result,
              buffer,
              LoadString(
                FindResourceHInstance(resStringRec.Module^),
                resStringRec.Identifier,
                buffer,
                SizeOf(buffer)));
          end
          else
            Result := PChar(resStringRec.Identifier);
        end
        else
          Result := '';

      except
        Result := '';
      end;
    end;

    function trLoadResString(resStringRec: PResStringRec): String;
    asm
      PUSH  EBP
      MOV   EBP, ESP
      ADD   ESP, $-8
      MOV   [EBP-$8], EDX
      MOV   [EBP-$4], EAX
      MOV   EDX, [EBP-$8]
      MOV   EAX, [EBP-$4]
      MOV   ECX, OFFSET Addr(TranslateLoadResString)-$1
      CALL  ECX
      MOV   ESP,EBP
      POP   EBP
    end;

    end.

Now finally the C# code which uses that DLL is given below:

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace TestWinform
{
    public partial class Form1 : Form
    {
        [DllImport("DelphiDllTest.dll")]
        public static extern void TestSwitching();

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            TestSwitching();            
        }
    }
}

Hope this is enough to generate the AV.


Solution

  • I believe that DEP is responsible for the exceptions. The problem is with the protection option that you are using. Instead of PAGE_READWRITE you need to use PAGE_EXECUTE_READWRITE. In Open, you need to protect @trLoadResString as well as @LoadResString, you are missing that call to VirtualProtect.

    Perhaps like this:

    VirtualProtect(@LoadResString, SIZE_C, PAGE_EXECUTE_READWRITE, @protect);
    Move((@LoadResString)^, loadResStringBuffer, SIZE_C);
    VirtualProtect(@LoadResString, SIZE_C, protect, @protect);
    FlushInstructionCache(GetCurrentProcess, @LoadResString, SIZE_C);
    
    VirtualProtect(@trLoadResString, SIZE_C, PAGE_EXECUTE_READWRITE, @protect);
    Move((@trLoadResString)^, (@LoadResString)^, SIZE_C);
    VirtualProtect(@trLoadResString, SIZE_C, protect, @protect);
    FlushInstructionCache(GetCurrentProcess, @trLoadResString, SIZE_C);
    

    And you need to do something similar in the Close method. Perhaps you need to restore trLoadResString.

    Your exported function is using the register calling convention, but it should export as stdcall. The export directive is ignored. It's only exports that matters.