fix: Read Windows total physical memory via GlobalMemoryStatusEx instead of wmic.exe - #3894
fix: Read Windows total physical memory via GlobalMemoryStatusEx instead of wmic.exe#3894cf-rhett wants to merge 1 commit into
Conversation
The IO device-context enricher shelled out to `wmic.exe` (falling back to PowerShell) to read `TotalPhysicalMemory` on Windows. WMIC is deprecated and absent on recent Windows installs, and spawning a child process for every enriched event is both slow and noisy for endpoint-security tooling that flags `wmic.exe` executions. Read the value directly from the kernel via `GlobalMemoryStatusEx` (`kernel32.dll`) using `dart:ffi`, matching the previous byte-valued semantics and removing the process spawn entirely. Adds the `ffi` package dependency, already used by `sentry_flutter`.
d5de069 to
26fc32f
Compare
| final globalMemoryStatusEx = DynamicLibrary.process().lookupFunction< | ||
| _GlobalMemoryStatusExNative, | ||
| _GlobalMemoryStatusExDart>('GlobalMemoryStatusEx'); |
There was a problem hiding this comment.
Bug: The code calls DynamicLibrary.process(), which is unsupported on Windows. This will always throw an error, causing memory information to be null.
Severity: HIGH
Suggested Fix
Instead of DynamicLibrary.process(), use DynamicLibrary.open('kernel32.dll') to load the Windows kernel library and then look up the GlobalMemoryStatusEx function. This is the correct pattern for accessing Windows APIs via FFI.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: packages/dart/lib/src/event_processor/enricher/io_platform_memory.dart#L37-L39
Potential issue: The function `_getWindowsTotalPhysicalMemory` attempts to use
`DynamicLibrary.process()` to look up a system function on Windows. However,
`DynamicLibrary.process()` is only supported on POSIX systems and throws an
`UnsupportedError` on Windows. The code catches this exception and returns `null`,
meaning total physical memory will never be reported on Windows systems. This is a
functional regression and will cause the associated unit test, which expects a non-null
value, to fail on the Windows CI runner.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Thanks for the flag — I checked this and it isn't a real bug. DynamicLibrary.process() is supported on Windows and does resolve GlobalMemoryStatusEx; it does not throw UnsupportedError.
Why process() works on Windows
The API contract for DynamicLibrary.process() is platform-neutral — it holds all globally-loaded symbols, "including the executable itself." On Windows the runtime resolves a symbol by walking every loaded module and calling GetProcAddress on each. See the Win32 code path in the SDK (pinned to tag 3.12.2):
LookupSymbolInProcess—EnumProcessModules+GetProcAddress#if defined(DART_HOST_OS_WINDOWS)dispatch for process lookups
GlobalMemoryStatusEx is exported by Kernel32.dll (Requirements → DLL: Kernel32.dll), which is always mapped into every Win32 process, so it's always in that enumeration and resolves.
Empirical check on real Windows
I ran a standalone dart:ffi parity check comparing process(), open('kernel32.dll'), wmic, and PowerShell CIM. All four agree to the byte:
sizeOf<MemoryStatusEx> = 64 (expect 64)
Native GlobalMemoryStatusEx:
[process()] ullTotalPhys = 8580235264
[open('kernel32.dll')] ullTotalPhys = 8580235264
wmic TotalPhysicalMemory = 8580235264
powershell CIM Total = 8580235264
PASS: native (8580235264) == wmic (8580235264)
So process() returns a correct, non-null value and the unit test passes on Windows.
Note your suggested fix (open('kernel32.dll')) is also correct and returns the identical value (second line above). I chose process() deliberately: kernel32.dll is guaranteed to already be mapped, so there's no need to acquire a fresh module handle. Happy to switch to open('kernel32.dll') if the team finds it clearer.
Parity check script (verify-winmem.ps1) run in a Windows guest
# Self-contained Windows physical-memory parity check.
# Writes a pure dart:ffi program (no pub get needed) and runs it, comparing
# native GlobalMemoryStatusEx against wmic and PowerShell CIM.
#
# Prereq: Dart SDK at C:\dart\dart-sdk\bin\dart.exe (adjust $dart if elsewhere).
# Run in the guest: powershell -NoProfile -ExecutionPolicy Bypass -File verify-winmem.ps1
$ErrorActionPreference = 'Stop'
$dartFile = Join-Path $env:TEMP 'verify-winmem.dart'
$dart = (Get-Command dart -ErrorAction SilentlyContinue).Source
if (-not $dart) {
Write-Error "dart not found on PATH. Install the SDK first, then re-run."
exit 1
}
Write-Output "Using dart: $dart"
$src = @'
// Standalone parity check: native GlobalMemoryStatusEx vs wmic vs PowerShell.
import 'dart:ffi';
import 'dart:io';
final class MemoryStatusEx extends Struct {
@Uint32()
external int dwLength;
@Uint32()
external int dwMemoryLoad;
@Uint64()
external int ullTotalPhys;
@Uint64()
external int ullAvailPhys;
@Uint64()
external int ullTotalPageFile;
@Uint64()
external int ullAvailPageFile;
@Uint64()
external int ullTotalVirtual;
@Uint64()
external int ullAvailVirtual;
@Uint64()
external int ullAvailExtendedVirtual;
}
typedef _GmseNative = Int32 Function(Pointer<MemoryStatusEx>);
typedef _GmseDart = int Function(Pointer<MemoryStatusEx>);
typedef _AllocNative = Pointer<Void> Function(Uint32, IntPtr);
typedef _AllocDart = Pointer<Void> Function(int, int);
typedef _FreeNative = Pointer<Void> Function(Pointer<Void>);
typedef _FreeDart = Pointer<Void> Function(Pointer<Void>);
int? nativeTotalPhys(DynamicLibrary lib) {
final gmse = lib.lookupFunction<_GmseNative, _GmseDart>('GlobalMemoryStatusEx');
final alloc = lib.lookupFunction<_AllocNative, _AllocDart>('GlobalAlloc');
final free = lib.lookupFunction<_FreeNative, _FreeDart>('GlobalFree');
final ptr = alloc(0x0040, sizeOf<MemoryStatusEx>()).cast<MemoryStatusEx>();
if (ptr.address == 0) return null;
try {
ptr.ref.dwLength = sizeOf<MemoryStatusEx>();
if (gmse(ptr) == 0) return null;
return ptr.ref.ullTotalPhys;
} finally {
free(ptr.cast());
}
}
int? tryLib(String label, DynamicLibrary Function() open) {
try {
final v = nativeTotalPhys(open());
print(' [$label] ullTotalPhys = $v');
return v;
} catch (e) {
print(' [$label] FAILED: $e');
return null;
}
}
int? wmicValue() {
try {
final r = Process.runSync('wmic', [
'ComputerSystem',
'get',
'TotalPhysicalMemory',
'/VALUE',
]);
final m = RegExp(r'TotalPhysicalMemory=(\d+)').firstMatch(r.stdout as String);
return m == null ? null : int.parse(m.group(1)!);
} catch (e) {
print(' wmic FAILED: $e');
return null;
}
}
int? psValue() {
try {
final r = Process.runSync('powershell', [
'-NoProfile',
'-Command',
'(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory',
]);
return int.tryParse((r.stdout as String).trim());
} catch (e) {
print(' powershell FAILED: $e');
return null;
}
}
void main() {
print('sizeOf<MemoryStatusEx> = ${sizeOf<MemoryStatusEx>()} (expect 64)');
print('Native GlobalMemoryStatusEx:');
final proc = tryLib('process()', DynamicLibrary.process);
final k32 = tryLib("open('kernel32.dll')", () => DynamicLibrary.open('kernel32.dll'));
final wmic = wmicValue();
final ps = psValue();
print('');
print('wmic TotalPhysicalMemory = $wmic');
print('powershell CIM Total = $ps');
final native = proc ?? k32;
final ok = native != null && wmic != null && native == wmic;
print('');
print(
ok
? 'PASS: native ($native) == wmic ($wmic)'
: 'FAIL: process()=$proc kernel32=$k32 wmic=$wmic powershell=$ps',
);
}
'@
Set-Content -Path $dartFile -Value $src -Encoding UTF8
Write-Output "Wrote $dartFile"
Write-Output "Running..."
& $dart $dartFile
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3894 +/- ##
==========================================
+ Coverage 87.46% 87.58% +0.11%
==========================================
Files 338 338
Lines 12282 12262 -20
==========================================
- Hits 10743 10740 -3
+ Misses 1539 1522 -17
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
📜 Description
On Windows, the pure-Dart IO device-context enricher (
PlatformMemory) readTotalPhysicalMemoryby shelling out towmic.exe(wmic ComputerSystem get TotalPhysicalMemory /VALUE), falling back topowershell.exewhen WMIC was absent. This replaces both process spawns with a direct kernel call toGlobalMemoryStatusExfromkernel32.dllviadart:ffi, readingMEMORYSTATUSEX.ullTotalPhys.The returned value keeps the previous semantics (bytes), so
device.memorySizeis unchanged. TheuseWindowsWmci/useWindowsPowerShellprobing, the WMIC/PowerShell helpers, and thewmic.exe/powershell.exeexistence checks are all removed. Addsffi: ^2.0.0as a dependency — already used bysentry_flutter.💡 Motivation and Context
WMIC is deprecated and being removed from current Windows installs. Two problems with the current approach:
wmic.exe(orpowershell.exe) child process under the host application. EDR/XDR tooling flags these spawns, and users ask why the app is invoking a deprecated Windows utility.GlobalMemoryStatusExis the documented Win32 API for this value, needs no child process, and works regardless of whether WMIC is installed. The library the current code was copied from (system_info2/onepub-dev/system_info) still uses WMIC on itsmasterbranch, so there's no upstream version to lean on here.💚 How did you test it?
Covered by the existing
test/event_processor/enricher/io_platform_memory_test.dart, which asserts a non-null, positiveTotalPhysicalMemoryon Windows and Linux andnullelsewhere.dart analyzeanddart formatare clean on the change.📝 Checklist
sendDefaultPiiis enabledChangelog Entry
Read Windows total physical memory via the native
GlobalMemoryStatusExAPI instead of spawning the deprecatedwmic.exe(or PowerShell) process.