diff --git a/WhiteMagic/Discovery/PeHeaderParser.cs b/WhiteMagic/Discovery/PeHeaderParser.cs
index d92b914..76b5751 100644
--- a/WhiteMagic/Discovery/PeHeaderParser.cs
+++ b/WhiteMagic/Discovery/PeHeaderParser.cs
@@ -112,6 +112,15 @@ public sealed class PeHeaderParser
/// The exported symbol name (case-sensitive, as stored
/// in the export name table).
/// The absolute address of the export in the target process.
+ ///
+ /// Forwarders are resolved by locating the target module in the process's loaded-module
+ /// list. API-set forwarders (virtual api-ms-win-* / ext-ms-* names) are NOT
+ /// supported: those are not real loaded modules, so resolution through the module list is
+ /// impossible without parsing the API-set schema — such a forwarder throws
+ /// . On modern Windows many system-DLL exports forward
+ /// through API sets; resolve those via the OS loader (GetProcAddress) instead.
+ /// Ordinal forwarders (Module.#N) are likewise unsupported.
+ ///
/// The export is not present.
/// The export forwards to an ordinal or to a
/// module (such as an API set) that is not resolvable from the target's module list.
@@ -212,7 +221,10 @@ public sealed class PeHeaderParser
private IntPtr ResolveForwarder(string forwarder, int depth)
{
- int dot = forwarder.LastIndexOf('.');
+ // A forwarder is "Module.Function"; the module name carries no extension, so the
+ // FIRST dot is the boundary. Splitting on the last dot would misparse export names
+ // that themselves contain a dot (e.g. some C++/managed exports).
+ int dot = forwarder.IndexOf('.');
if (dot <= 0 || dot >= forwarder.Length - 1)
throw new InvalidDataException($"Malformed export forwarder string '{forwarder}'.");
diff --git a/WhiteMagic/RemoteFunction.cs b/WhiteMagic/RemoteFunction.cs
index e353063..342a8ec 100644
--- a/WhiteMagic/RemoteFunction.cs
+++ b/WhiteMagic/RemoteFunction.cs
@@ -53,10 +53,21 @@ public sealed class RemoteFunction
///
/// Creates a managed delegate bound to this function for the in-process scenario.
- /// Only valid when the session was opened in-process.
///
+ /// The session is not in-process. The
+ /// resolved lives in the target process; a delegate to it would
+ /// access-violate when invoked from the host, so this is rejected for external sessions.
+ /// Use (remote thread) for external targets.
public TDelegate CreateDelegate() where TDelegate : Delegate
{
+ if (_magic.Memory is not InProcessReader)
+ {
+ throw new InvalidOperationException(
+ "CreateDelegate is only valid for an in-process session (Magic.OpenInProcess). " +
+ "The function address is not mapped into the host process for an external target; " +
+ "use Execute to call it via a remote thread.");
+ }
+
return new InProcessInvoker(_magic.Memory).CreateFunction(Address);
}
}
diff --git a/WhiteMagicTest/Execution/RemoteThreadExecutorTests.cs b/WhiteMagicTest/Execution/RemoteThreadExecutorTests.cs
index 161c8fa..adb1f1c 100644
--- a/WhiteMagicTest/Execution/RemoteThreadExecutorTests.cs
+++ b/WhiteMagicTest/Execution/RemoteThreadExecutorTests.cs
@@ -38,8 +38,9 @@ public sealed class RemoteThreadExecutorTests
// Five-arg callee that also executes an alignment-sensitive SSE instruction, proving
// the stub delivers a 16-byte-aligned stack the CPU actually accepts (movaps #GPs on a
// misaligned address) alongside correct register+stack argument placement.
- // sub rsp, 24 ; entry rsp ≡ 8 (mod 16) -> rsp ≡ 0 (16-aligned), 16-byte
- // ; scratch at [rsp..rsp+16) that clears the return slot ([rsp+24])
+ // sub rsp, 24 ; entry rsp ≡ 8 (mod 16) -> rsp ≡ 0 (16-aligned), giving a
+ // ; 16-byte aligned scratch at [rsp..rsp+16) below the saved
+ // ; return address ([rsp+24]) so the store leaves it intact
// movaps [rsp], xmm0 ; aligned 16-byte store — faults unless rsp is 16-aligned
// add rsp, 24 ; restore
// mov eax, ecx
diff --git a/WhiteMagicTest/ModuleFunctionTests.cs b/WhiteMagicTest/ModuleFunctionTests.cs
index 68cff17..c962efb 100644
--- a/WhiteMagicTest/ModuleFunctionTests.cs
+++ b/WhiteMagicTest/ModuleFunctionTests.cs
@@ -79,6 +79,32 @@ public class ModuleFunctionTests
Assert.Throws(() => magic["kernel32"]["NoSuchExport_ZZZ"]);
}
+ private delegate uint GetCurrentProcessIdDelegate();
+
+ [Fact]
+ public void CreateDelegate_throws_for_external_session()
+ {
+ Load("kernel32.dll");
+
+ // External reader (even to self): the address is not treated as host-mapped, so a
+ // delegate to it is rejected rather than handed back to AV on invocation.
+ using var magic = Magic.Open(Process.GetCurrentProcess());
+ RemoteFunction fn = magic["kernel32"]["GetCurrentProcessId"];
+
+ Assert.Throws(() => fn.CreateDelegate());
+ }
+
+ [Fact]
+ public void CreateDelegate_invokes_function_in_process()
+ {
+ Load("kernel32.dll");
+
+ using var magic = Magic.OpenInProcess();
+ var getPid = magic["kernel32"]["GetCurrentProcessId"].CreateDelegate();
+
+ Assert.Equal((uint)Process.GetCurrentProcess().Id, getPid());
+ }
+
[Fact]
public void Resolved_function_executes_via_remote_thread()
{