Skip to content

Replace WMI with NtQueryInformationProcess for process command line retrieval#13539

Draft
JanProvaznik wants to merge 1 commit intodotnet:mainfrom
JanProvaznik:janprovaznik/replace-wmi-with-ntquery
Draft

Replace WMI with NtQueryInformationProcess for process command line retrieval#13539
JanProvaznik wants to merge 1 commit intodotnet:mainfrom
JanProvaznik:janprovaznik/replace-wmi-with-ntquery

Conversation

@JanProvaznik
Copy link
Copy Markdown
Member

@JanProvaznik JanProvaznik commented Apr 14, 2026

Summary

Replace the Windows implementation of TryGetCommandLine in ProcessExtensions.cs to use NtQueryInformationProcess with ProcessCommandLineInformation (info class 60) instead of WMI COM queries.

Fixes #13522

Problem

The previous implementation queried WMI (Win32_Process) via COM with WBEM_INFINITE timeout to retrieve process command lines during node reuse scanning. When the WMI service was unresponsive, corrupted, or overloaded (common with third-party endpoint protection/monitoring software), this caused an indefinite hang on the calling thread, freezing the entire build.

The hang was observed in a dump with the main thread blocked at ZwAlpcSendWaitReceivePort waiting for the WMI RPC response that never came.

Fix

Use NtQueryInformationProcess with ProcessCommandLineInformation — a direct kernel query with no service dependency. This matches the pattern already used by the Linux (/proc/pid/cmdline) and macOS (sysctl KERN_PROCARGS2) implementations in the same file.

Benefits

  • Eliminates infinite hang risk — no WMI service dependency
  • Faster — single syscall vs cross-process RPC to WMI service
  • Lower privilegePROCESS_QUERY_LIMITED_INFORMATION vs WMI impersonation
  • Removes ~450 lines of COM interface definitions
  • Consistent — all three platforms now use direct OS queries

Testing

  • Existing TryGetCommandLine_RunningProcess_ContainsExpectedExecutable — passes
  • Existing TryGetCommandLine_RunningProcess_ContainsArguments — passes
  • Framework project builds with 0 warnings, 0 errors

Context

The WMI-based code path is gated behind ChangeWave 18.5 (node mode filtering). Users hitting the hang can work around it by setting MSBUILDDISABLEFEATURESFROMVERSION=18.5, but this fix eliminates the root cause.

@baronfel
Copy link
Copy Markdown
Member

cc @JeremyKuhne who has some stuff coming down the pipe for this as well

@JanProvaznik JanProvaznik force-pushed the janprovaznik/replace-wmi-with-ntquery branch from fe76728 to bf09eee Compare April 14, 2026 16:27
@JanProvaznik JanProvaznik self-assigned this Apr 14, 2026
@JanProvaznik JanProvaznik marked this pull request as ready for review April 14, 2026 16:29
Copilot AI review requested due to automatic review settings April 14, 2026 16:29
@JanProvaznik
Copy link
Copy Markdown
Member Author

/review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

Expert Code Review (command) failed. Please review the logs for details.

Review complete. The expert-reviewer sub-agent posted inline comments and a final COMMENT verdict on PR #13539. No additional action needed from the orchestrator.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates MSBuild’s Windows implementation of ProcessExtensions.TryGetCommandLine to avoid WMI/COM queries (which can hang indefinitely when WMI is unhealthy) by switching to a direct NtQueryInformationProcess call using ProcessCommandLineInformation (info class 60).

Changes:

  • Replaced Windows command-line retrieval from WMI (Win32_Process) to NtQueryInformationProcess(ProcessCommandLineInformation).
  • Removed the large set of WMI COM interop declarations/constants previously required for the query.
  • Added SafeProcessHandle interop for OpenProcess.

Comment thread src/Framework/Utilities/ProcessExtensions.cs
Comment thread src/Framework/Utilities/ProcessExtensions.cs
…etrieval

The Windows implementation of TryGetCommandLine used WMI (Win32_Process query
via COM) with WBEM_INFINITE timeout, which could hang indefinitely when the
WMI service was unresponsive, corrupted, or overloaded by third-party providers.
This blocked the main thread during node reuse scanning, freezing the entire
build.

Replace the WMI approach with NtQueryInformationProcess using
ProcessCommandLineInformation (info class 60, available since Windows 8.1).
This is a direct kernel query with no service dependency, matching the pattern
used by Linux (/proc/pid/cmdline) and macOS (sysctl KERN_PROCARGS2).

Benefits:
- Eliminates infinite hang risk (no WMI service dependency)
- Faster: single syscall vs cross-process RPC
- Lower privilege: PROCESS_QUERY_LIMITED_INFORMATION vs WMI impersonation
- Removes ~450 lines of COM interface definitions
- Consistent with Linux/macOS: direct OS query, no service dependency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@JanProvaznik JanProvaznik force-pushed the janprovaznik/replace-wmi-with-ntquery branch from bf09eee to 5a1c4da Compare April 14, 2026 16:38
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary: WMI → NtQueryInformationProcess migration

Overall: Solid improvement 🟢 — no blocking issues found.

Replacing WMI COM interop with NtQueryInformationProcess is a clear win: eliminates ~400 lines of COM interface boilerplate, removes the problematic process-wide CoInitializeSecurity call, and is dramatically simpler and faster. The memory management is correct (no leak paths), the UNICODE_STRING pointer interpretation is correct, and SafeProcessHandle is properly used.

Findings by Severity

Severity Count Summary
BLOCKING 0
MAJOR 0
MINOR 3 LibraryImport consistency, initial buffer size, returnLength guard
NIT 1 Documentation of API stability / version requirement

Technical Correctness Verified

  • UNICODE_STRING interpretation: Buffer pointer is an absolute address pointing into the caller's allocation. Marshal.PtrToStructure + Marshal.PtrToStringUni reads this correctly.
  • Memory management: Marshal.AllocHGlobal / FreeHGlobal properly paired via finally block. No leak on resize path (old buffer freed before new allocation, OOM propagates safely).
  • Handle management: SafeProcessHandle with using declaration ensures CloseHandle on all paths.
  • x86/x64 struct layout: LayoutKind.Sequential without explicit Pack correctly handles the 4-byte padding on x64 between MaximumLength and Buffer.
  • Error handling: All failure paths return null, caller has catch { return false; }, so any unexpected exception is gracefully handled.

Existing Precedent

NativeMethods.cs already P/Invokes NtQueryInformationProcess (with ProcessBasicInformation for parent PID retrieval), so this is not the first use of ntdll.dll APIs in the codebase.

Test Coverage

Existing tests in ProcessExtensions_Tests.cs cover the TryGetCommandLine path with live processes on Windows, verifying both executable name and argument retrieval. These should exercise the new code path on Windows CI.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

  • #13539 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
  • #13539 pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by Expert Code Review (command) for issue #13539 · ● 4.4M

Comment on lines +191 to +194
[DllImport("kernel32.dll", SetLastError = true)]
private static extern SafeProcessHandle OpenProcess(int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId);

[DllImport("ntdll.dll")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — Style: DllImportLibraryImport

The BSD class (line 290) uses source-generated LibraryImport, but the new Windows class uses the older runtime-interpreted DllImport. For consistency within this file and to benefit from compile-time source generation (avoids runtime marshaling stubs), consider migrating to LibraryImport here. The class would need to become partial and the methods static partial:

private static partial class Windows
{
    [LibraryImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial SafeProcessHandle OpenProcess(int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId);

    [LibraryImport("ntdll.dll")]
    private static partial int NtQueryInformationProcess(
        SafeProcessHandle processHandle,
        int processInformationClass,
        IntPtr processInformation,
        int processInformationLength,
        out int returnLength);
}

Not blocking — DllImport works correctly — but the inconsistency stands out since the file already uses the modern pattern.

Comment thread src/Framework/Utilities/ProcessExtensions.cs
Comment thread src/Framework/Utilities/ProcessExtensions.cs
Comment thread src/Framework/Utilities/ProcessExtensions.cs
Copy link
Copy Markdown
Member

@JeremyKuhne JeremyKuhne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to see this change, but we need to find official documentation to be able to use this. @baronfel if we can't find it, we can ask Windows to document.

@JeremyKuhne
Copy link
Copy Markdown
Member

Note that I have a commit that I'm creating that moves to direct COM usage. That doesn't fix the WMI dependency but does remove the COM interop overhead. (Again, would much rather not use WMI.)

@JanProvaznik JanProvaznik marked this pull request as draft April 15, 2026 07:07
@MichalPavlik
Copy link
Copy Markdown
Member

Note that I have a commit that I'm creating that moves to direct COM usage. That doesn't fix the WMI dependency but does remove the COM interop overhead. (Again, would much rather not use WMI.)

I tried to replace the WMI with IDebugClient: #13560

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dotnet run --file hangs during restore when PackageVersion is missing

5 participants