Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/DiffEngine.Tests/Wow64CommandLineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#if NET10_0
/// <summary>
/// Reading the command line of a 32-bit process from this 64-bit one.
/// <para>
/// NtQueryInformationProcess with ProcessBasicInformation answers a 64-bit caller with the 64-bit
/// PEB, even when the target is running under WOW64. Reading that with 32-bit offsets produced
/// nothing, so every 32-bit diff tool - which is most of the %ProgramFiles(x86)% installs the
/// resolver goes out of its way to find - had no command line: never seen as already running, and
/// never killed. The 32-bit PEB has to be asked for by name.
/// </para>
/// </summary>
[NotInParallel]
[RunOn(TUnit.Core.Enums.OS.Windows)]
public class Wow64CommandLineTests
{
[Test]
public async Task AThirtyTwoBitProcessHasAReadableCommandLine()
{
var wow = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Windows),
"SysWOW64",
"cmd.exe");
if (!Environment.Is64BitProcess ||
!File.Exists(wow))
{
// A 32-bit host, or a Windows with no WOW64 layer. Nothing to say here
return;
}

// Distinctive, so this cannot match any other cmd on the machine
var marker = $"DiffEngineWow64Probe{Guid.NewGuid():N}";
var process = Process.Start(
new ProcessStartInfo
{
FileName = wow,
// Shaped like a diff tool invocation, because FindAll only keeps command
// lines with two file path arguments
Arguments = $"/c ping -n 30 127.0.0.1 >nul & rem C:\\probe\\{marker}.received.txt C:\\probe\\{marker}.verified.txt",
UseShellExecute = false,
CreateNoWindow = true
})!;

try
{
await Assert.That(await WaitForCommandLine(marker)).IsTrue();
}
finally
{
try
{
if (!process.HasExited)
{
process.Kill();
}
}
catch
{
// Nothing useful to do if it has already gone
}

process.Dispose();
}
}

static async Task<bool> WaitForCommandLine(string marker)
{
for (var attempt = 0; attempt < 40; attempt++)
{
var found = WindowsProcess
.FindAll([with(StringComparer.OrdinalIgnoreCase), "cmd.exe"])
.Any(_ => _.Command.Contains(marker, StringComparison.Ordinal));
if (found)
{
return true;
}

await Task.Delay(250);
}

return false;
}
}
#endif
62 changes: 59 additions & 3 deletions src/DiffEngine/Process/WindowsProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ private static partial int NtQueryInformationProcess(
int size,
out int returnLength);

/// <summary>
/// The same export, asked for a pointer sized answer rather than a struct: the ProcessWow64
/// Information class returns the address of a WOW64 target's 32-bit PEB.
/// </summary>
[LibraryImport("ntdll.dll", EntryPoint = "NtQueryInformationProcess")]
private static partial int NtQueryWow64Peb(
SafeProcessHandle handle,
int processInformationClass,
ref IntPtr info,
int size,
out int returnLength);

[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool IsWow64Process(
Expand Down Expand Up @@ -89,6 +101,14 @@ static extern bool ReadProcessMemory(
IntPtr size,
out IntPtr bytesRead);

[DllImport("ntdll.dll", EntryPoint = "NtQueryInformationProcess")]
static extern int NtQueryWow64Peb(
SafeProcessHandle handle,
int processInformationClass,
ref IntPtr info,
int size,
out int returnLength);

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool IsWow64Process(SafeProcessHandle handle, out bool isWow64);
#endif
Expand All @@ -99,6 +119,12 @@ static extern bool ReadProcessMemory(
const int processTerminate = 0x0001;
const int processBasicInformation = 0;

/// <summary>
/// ProcessWow64Information. Returns the address of the 32-bit PEB for a WOW64 target, or zero
/// for one that is not running under WOW64.
/// </summary>
const int processWow64Information = 26;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct PROCESSENTRY32W
{
Expand Down Expand Up @@ -298,16 +324,46 @@ static int FindDotSeparatedPath(CharSpan span)
return null;
}

return Environment.Is64BitProcess && !isTarget32Bit
? ReadCommandLine64(handle, pbi.PebBaseAddress)
: ReadCommandLine32(handle, pbi.PebBaseAddress);
if (!Environment.Is64BitProcess)
{
// A 32-bit caller cannot reach a 64-bit target's PEB, so there is nothing useful to
// read rather than something wrong to read
if (Environment.Is64BitOperatingSystem &&
!isTarget32Bit)
{
return null;
}

return ReadCommandLine32(handle, pbi.PebBaseAddress);
}

if (!isTarget32Bit)
{
return ReadCommandLine64(handle, pbi.PebBaseAddress);
}

// A WOW64 target seen from a 64-bit caller. ProcessBasicInformation answers with the
// 64-bit PEB the system keeps for it, and reading that with 32-bit offsets yields
// nothing usable - so every 32-bit diff tool, which is most of the %ProgramFiles(x86)%
// installs the resolver goes out of its way to find, had no command line at all: never
// detected as already running, and never killed. The 32-bit PEB has to be asked for
return TryGetWow64Peb(handle, out var wow64Peb)
? ReadCommandLine32(handle, wow64Peb)
: null;
}
catch
{
return null;
}
}

static bool TryGetWow64Peb(SafeProcessHandle handle, out IntPtr peb)
{
peb = IntPtr.Zero;
return NtQueryWow64Peb(handle, processWow64Information, ref peb, IntPtr.Size, out _) == 0 &&
peb != IntPtr.Zero;
}

static string? ReadCommandLine64(SafeProcessHandle handle, IntPtr pebAddress)
{
// In 64-bit PEB, ProcessParameters is at offset 0x20
Expand Down
Loading