Skip to content

fix: Read Windows total physical memory via GlobalMemoryStatusEx instead of wmic.exe - #3894

Open
cf-rhett wants to merge 1 commit into
getsentry:mainfrom
cf-rhett:rhett/fix/windows-physical-memory-native
Open

fix: Read Windows total physical memory via GlobalMemoryStatusEx instead of wmic.exe#3894
cf-rhett wants to merge 1 commit into
getsentry:mainfrom
cf-rhett:rhett/fix/windows-physical-memory-native

Conversation

@cf-rhett

@cf-rhett cf-rhett commented Jul 15, 2026

Copy link
Copy Markdown

📜 Description

On Windows, the pure-Dart IO device-context enricher (PlatformMemory) read TotalPhysicalMemory by shelling out to wmic.exe (wmic ComputerSystem get TotalPhysicalMemory /VALUE), falling back to powershell.exe when WMIC was absent. This replaces both process spawns with a direct kernel call to GlobalMemoryStatusEx from kernel32.dll via dart:ffi, reading MEMORYSTATUSEX.ullTotalPhys.

The returned value keeps the previous semantics (bytes), so device.memorySize is unchanged. The useWindowsWmci / useWindowsPowerShell probing, the WMIC/PowerShell helpers, and the wmic.exe/powershell.exe existence checks are all removed. Adds ffi: ^2.0.0 as a dependency — already used by sentry_flutter.

💡 Motivation and Context

WMIC is deprecated and being removed from current Windows installs. Two problems with the current approach:

  1. Endpoint-security noise. Every enriched event triggers a wmic.exe (or powershell.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.
  2. Cost. Spawning a process to read a single integer is far heavier than a syscall — the caching added in [Flutter] startTransaction slows down grpc Requests on windows #2760 reduced how often it happens, but the spawn is still there on the first enriched event.

GlobalMemoryStatusEx is 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 its master branch, 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, positive TotalPhysicalMemory on Windows and Linux and null elsewhere. dart analyze and dart format are clean on the change.

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes (existing test covers the Windows path)
  • No new PII added or SDK only sends newly added PII if sendDefaultPii is enabled
  • I updated the docs if needed
  • All tests passing
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec
  • No breaking changes

Changelog Entry

Read Windows total physical memory via the native GlobalMemoryStatusEx API instead of spawning the deprecated wmic.exe (or PowerShell) process.

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`.
@cf-rhett
cf-rhett force-pushed the rhett/fix/windows-physical-memory-native branch from d5de069 to 26fc32f Compare July 15, 2026 14:33
@cf-rhett
cf-rhett marked this pull request as ready for review July 15, 2026 15:09
Comment on lines +37 to +39
final globalMemoryStatusEx = DynamicLibrary.process().lookupFunction<
_GlobalMemoryStatusExNative,
_GlobalMemoryStatusExDart>('GlobalMemoryStatusEx');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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):

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

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 23.07692% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.58%. Comparing base (0a49f83) to head (26fc32f).
⚠️ Report is 15 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...c/event_processor/enricher/io_platform_memory.dart 23.07% 10 Missing ⚠️
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     
Flag Coverage Δ
sentry 87.70% <23.07%> (+0.22%) ⬆️
sentry_dio 97.73% <ø> (ø)
sentry_drift 93.57% <ø> (ø)
sentry_file 65.29% <ø> (ø)
sentry_firebase_remote_config 100.00% <ø> (ø)
sentry_flutter 91.49% <ø> (ø)
sentry_grpc 99.09% <ø> (ø)
sentry_hive 77.48% <ø> (ø)
sentry_isar 74.37% <ø> (ø)
sentry_link 21.50% <ø> (ø)
sentry_logging 97.01% <ø> (ø)
sentry_sqflite 88.81% <ø> (ø)
sentry_supabase 97.27% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@buenaflor
buenaflor requested a review from lucas-zimerman July 28, 2026 08:35
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.

1 participant