Skip to content

Windows: rewrite process enumeration around NtQuerySystemInformation - #967

Open
CarterLi wants to merge 4 commits into
dalance:masterfrom
CarterLi:win
Open

Windows: rewrite process enumeration around NtQuerySystemInformation#967
CarterLi wants to merge 4 commits into
dalance:masterfrom
CarterLi:win

Conversation

@CarterLi

@CarterLi CarterLi commented Sep 4, 2026

Copy link
Copy Markdown

Note: I made the high-level design, and let AI write the code (with my small fixes here and there). It's still carefully tested and benchmarked.


Summary

Rewrites the Windows process backend around a single NtQuerySystemInformation (NQSI) snapshot, replacing the previous per-process OpenProcessGetProcessTimes / GetProcessMemoryInfo / GetProcessIoCounters / GetPriorityClass / EnumProcessModulesEx walk. This cuts the syscall count from O(N × k) to O(1) for the enumeration phase and eliminates the race window where a process could exit between individual queries.

Changes

Performance

  • Single-snapshot enumeration. All per-process metrics (memory, CPU times, I/O counters, image name, thread count, PPID) are now read from one NQSI buffer. The old code opened each process handle twice (once for the delta baseline, once for the final read) and issued ~6 syscalls per handle.
  • Lazy SID-to-name resolution. LookupAccountSidW is deferred to column render time (SID_MAX::display_name()) and cached per SID value, instead of being called eagerly for every process during collection. This saves several milliseconds when many groups are present (especially for system processes).
  • Elevated fast path. When running with admin privileges, SystemFullProcessInformation (class 148) is used. It embeds each process's user SID directly in the snapshot, saving one OpenProcessToken + GetTokenInformation round-trip per process.

Correctness / bug fixes

  • System processes are no longer silently dropped. The old code required every field (command, start_time, cpu_info, memory_info, disk_info, user, groups, thread) to be non-None (all_ok gate); any OpenProcess failure (e.g. System, Registry, Memory Compression) caused the entire entry to be skipped. NQSI returns memory, CPU, I/O and image name for all processes including kernel threads, so they are now always listed. User / Group columns are left empty when the token cannot be opened.
  • Handle leaks fixed. OpenProcessToken handles were never closed in get_user / get_groups / set_privilege. Process handles are now managed by a ProcHandles RAII wrapper with a Drop impl.
  • SID name cache keyed by value, not pointer. The old HashMap<PSID, _> cache was keyed by the address of a transient allocation; once freed, the address could be recycled by the next query and return a different account's name. The cache is now keyed by SID_MAX (an owned, fixed-size 68-byte SID) with value equality.
  • Command lines fetched via NtQueryInformationProcess(ProcessCommandLineInformation) instead of EnumProcessModulesEx + GetModuleBaseNameW, which only returned the bare executable name. Fixes Running in windows it does not show command line arguments #752.

Other

  • SID_MAX value type. SIDs are stored as a fixed-size #[repr(C)] struct (embedding the official SID header + padding to SECURITY_MAX_SID_SIZE) instead of Vec<u64>, removing per-SID heap allocations and enabling Hash / Eq by live bytes.
  • Memory total now comes from GlobalMemoryStatusEx (ullTotalPhys) instead of GetPerformanceInfo (PhysicalTotal × PageSize). This reduced one system DLL dependency after the previous EnumProcessModulesEx, GetModuleBaseNameW and GetPerformanceInfo removal.

Behavior changes

Column Before After
Priority User-mode priority class (Normal, Idle, High, …) via GetPriorityClass Real-time, kernel level base priority (numeric) from the NQSI snapshot, which matchs the behaviors of other POSIX platforms.
User / Group Shown for every process (or the process was dropped) Empty for protected / system processes whose token cannot be opened without elevation.
Command Bare module name (e.g. svchost.exe) Full command line (e.g. C:\Windows\System32\svchost.exe -k netsvcs), falling back to the image name when the process cannot be opened.
System processes Silently absent Always visible; kernel-internal processes (a.k.a kthread: Idle, System, Registry, Memory Compression) are also available unless show_kthread is set to false.

Screenshots

Before, with or without admin permission

old-noadmin

After, without admin permission

new-noadmin

After, with admin permission

new-admin

Benchmark

$ hyperfine.exe -w 10 -S none c:/users/zhang/procs.old.exe c:/users/zhang/procs.new.exe
Benchmark 1: c:/users/zhang/procs.old.exe
  Time (mean ± σ):     156.2 ms ±   2.9 ms    [User: 21.7 ms, System: 20.0 ms]
  Range (min … max):   150.2 ms … 160.7 ms    18 runs

Benchmark 2: c:/users/zhang/procs.new.exe
  Time (mean ± σ):     143.6 ms ±   2.6 ms    [User: 17.2 ms, System: 25.0 ms]
  Range (min … max):   136.9 ms … 147.9 ms    20 runs

Summary
  c:/users/zhang/procs.new.exe ran
    1.09 ± 0.03 times faster than c:/users/zhang/procs.old.exe

Despite procs.new.exe reports about 2x more processes than procs.old.exe

Note

NtQuerySystemInformation and NtQueryInformationProcess are so called Native API, which are partially documented only. This PR does use some undocumented features.


My original PR message if you prefer
  1. Processes are enumerated around a single NtQuerySystemInformation (NQSI) snapshot instead of the previous per-process handle/token/module walk. This greatly reduced syscalls (GetProcessMemoryInfo, GetProcessTimes, GetProcessIoCounters, GetPriorityClass, EnumProcessModulesEx) per process, and therefore improved performance and reduced risks that processes may exit between syscalls.
  2. System processes are no longer dropped silently. Thanks to NQSI, memory info, CPU times, IO counters and process name are always available to all processes (including system processes and kernel threads), so we don't need to drop them. User / Group info is not available from the regular NQSI(SystemProcessInformation) call though. For system processes which we don't have to permission to open, User / Group info is left empty.
  3. When elevated, the advanced syscall NQSI(SystemFullProcessInformation) is used, which contains SID for system processes we don't even have permission to open with administrator permission. And since the syscall contains User SID, one more OpenProcessToken/GetTokenInformation is saved.
  4. Command lines are fetched via NtQueryInformationProcess(ProcessCommandLineInformation) for processes we have permission to open. Fixes Running in windows it does not show command line arguments #752
  5. User / Group info is now lazily fetched. This saved a few milliseconds since Windows applies multiple groups to one process, especially for system processes.
  6. Several bugs are fixed.
    1. The handles returned by OpenProcessToken are now correctly closed.
    2. SID caches are now keyed by value instead of pointer.

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.

Running in windows it does not show command line arguments

1 participant