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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.20.3] - 2026-06-08

### Fixed
- **Drive enumeration no longer crashes on missing WMI properties.** `FixedDriveService` read `MediaType`/`BusType` with `Convert.ToUInt32(value ?? 0u)`, but WMI returns `DBNull.Value` (not null) for absent properties, so `Convert.ToUInt32(DBNull.Value)` threw and aborted the whole scan on some hardware. Reads now go through a `ToUInt32Safe` helper that treats null and `DBNull` as 0.
- **Uninstaller trusted-directory check no longer accepts sibling folders.** `IsUnderTrustedDirectory` used a bare `StartsWith`, so `C:\Program Files Evil\…` passed the `C:\Program Files` check. It now compares on a normalized directory boundary (trailing separator) so only true sub-paths of a trusted directory are accepted.

## [1.20.2] - 2026-06-08

### Fixed
Expand Down
21 changes: 21 additions & 0 deletions SysManager/SysManager.Tests/FixedDriveServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,25 @@ public void FixedDrive_WithExpression_CreatesModifiedCopy()
Assert.Equal("NVMe", b.BusType);
Assert.Equal("", a.MediaType); // original unchanged
}

// Regression: WMI returns DBNull.Value (not null) for absent properties, and
// Convert.ToUInt32(DBNull.Value) throws — which crashed drive enumeration.
[Fact]
public void ToUInt32Safe_DBNull_ReturnsZero()
=> Assert.Equal(0u, FixedDriveService.ToUInt32Safe(DBNull.Value));

[Fact]
public void ToUInt32Safe_Null_ReturnsZero()
=> Assert.Equal(0u, FixedDriveService.ToUInt32Safe(null));

[Theory]
[InlineData(4, 4u)]
[InlineData((uint)17, 17u)]
[InlineData("11", 11u)]
public void ToUInt32Safe_ConvertibleValue_ReturnsValue(object input, uint expected)
=> Assert.Equal(expected, FixedDriveService.ToUInt32Safe(input));

[Fact]
public void ToUInt32Safe_Unconvertible_ReturnsZero()
=> Assert.Equal(0u, FixedDriveService.ToUInt32Safe("not-a-number"));
}
22 changes: 22 additions & 0 deletions SysManager/SysManager.Tests/UninstallerServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ namespace SysManager.Tests;
/// </summary>
public class UninstallerServiceTests
{
// ── IsUnderTrustedDirectory (regression: prefix-boundary bypass) ──

[Fact]
public void IsUnderTrustedDirectory_PathInsideProgramFiles_IsTrusted()
{
var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
Assert.True(UninstallerService.IsUnderTrustedDirectory(System.IO.Path.Combine(pf, "Vendor", "app.exe")));
}

[Fact]
public void IsUnderTrustedDirectory_SiblingWithSharedPrefix_IsNotTrusted()
{
// "C:\Program Files Evil\..." must NOT pass the "C:\Program Files" check.
var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var evil = pf + " Evil\\malware.exe";
Assert.False(UninstallerService.IsUnderTrustedDirectory(evil));
}

[Fact]
public void IsUnderTrustedDirectory_UntrustedLocation_IsNotTrusted()
=> Assert.False(UninstallerService.IsUnderTrustedDirectory(@"C:\Temp\random\app.exe"));

// ── ParseListTable ──

[Fact]
Expand Down
19 changes: 17 additions & 2 deletions SysManager/SysManager/Services/FixedDriveService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ public static IReadOnlyList<FixedDrive> Enumerate()
{
var id = mo["DeviceId"]?.ToString() ?? "";
media[id] = (
MapMedia(Convert.ToUInt32(mo["MediaType"] ?? 0u)),
MapBus(Convert.ToUInt32(mo["BusType"] ?? 0u)));
MapMedia(ToUInt32Safe(mo["MediaType"])),
MapBus(ToUInt32Safe(mo["BusType"])));
}
}

Expand Down Expand Up @@ -114,6 +114,21 @@ public static IReadOnlyList<FixedDrive> Enumerate()
return drives;
}

/// <summary>
/// Converts a WMI property value to uint, treating both null and
/// <see cref="DBNull"/> as 0. WMI returns <c>DBNull.Value</c> (not null) for
/// absent properties, and <c>Convert.ToUInt32(DBNull.Value)</c> throws —
/// which previously crashed drive enumeration on common hardware.
/// </summary>
internal static uint ToUInt32Safe(object? value)
{
if (value is null || value is DBNull) return 0u;
try { return Convert.ToUInt32(value); }
catch (InvalidCastException) { return 0u; }
catch (FormatException) { return 0u; }
catch (OverflowException) { return 0u; }
}

private static string MapMedia(uint v) => v switch
{
3 => "HDD",
Expand Down
11 changes: 9 additions & 2 deletions SysManager/SysManager/Services/UninstallerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ internal static (string Exe, string Args) ParseUninstallCommand(string command)
/// Checks whether the given absolute path resides under a trusted system directory
/// (Program Files, Windows, ProgramData, or LocalApplicationData).
/// </summary>
private static bool IsUnderTrustedDirectory(string fullPath)
internal static bool IsUnderTrustedDirectory(string fullPath)
{
var trustedDirs = new[]
{
Expand All @@ -375,8 +375,15 @@ private static bool IsUnderTrustedDirectory(string fullPath)
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
};

// Compare on a directory boundary, not a raw prefix. A bare StartsWith lets
// "C:\Program Files Evil\x.exe" pass the "C:\Program Files" check — so append
// a trailing separator to both sides before comparing.
static string WithSep(string p) =>
p.EndsWith(System.IO.Path.DirectorySeparatorChar) ? p : p + System.IO.Path.DirectorySeparatorChar;

var candidate = WithSep(System.IO.Path.GetFullPath(fullPath));
return trustedDirs.Any(dir =>
!string.IsNullOrEmpty(dir) &&
fullPath.StartsWith(dir, StringComparison.OrdinalIgnoreCase));
candidate.StartsWith(WithSep(System.IO.Path.GetFullPath(dir)), StringComparison.OrdinalIgnoreCase));
}
}
6 changes: 3 additions & 3 deletions SysManager/SysManager/SysManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<RootNamespace>SysManager</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>NU1603;NU1701</NoWarn>
<Version>1.20.2</Version>
<FileVersion>1.20.2.0</FileVersion>
<AssemblyVersion>1.20.2.0</AssemblyVersion>
<Version>1.20.3</Version>
<FileVersion>1.20.3.0</FileVersion>
<AssemblyVersion>1.20.3.0</AssemblyVersion>
<Product>SysManager</Product>
<Description>SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.</Description>
<PackageProjectUrl>https://github.com/laurentiu021/SystemManager</PackageProjectUrl>
Expand Down
Loading