Skip to content

Unhandled ArgumentException from IUpdate::get_IsDownloaded() aborts the entire cmdlet #71

Description

@ghosty212

Summary

When a single update object in the search result set throws on a property read, Get-WindowsUpdate / Install-WindowsUpdate terminate with a non-actionable error and return nothing at all. All other pending updates — including security updates that were enumerated successfully — are silently lost.

The underlying COM failure is environmental (a corrupted SoftwareDistribution datastore, see Root cause), but the module's lack of a guard around the property access turns a partial, per-update problem into a total failure of the cmdlet. The user-facing error contains no update title, no property name, and no indication that a Windows Update Agent reset is the remedy.

Install-WindowsUpdate : Der Wert liegt außerhalb des erwarteten Bereichs.
    + CategoryInfo          : NotSpecified: (:) [Get-WindowsUpdate], ArgumentException
    + FullyQualifiedErrorId : System.ArgumentException,PSWindowsUpdate.GetWindowsUpdate

(German locale; English equivalent is Value does not fall within the expected range.)

This appears to be the same failure reported in #14 and in numerous blog posts, where the advice is consistently "reset the Windows Update components" without an explanation of the mechanism. This report provides the stack trace and a minimal reproduction.

Environment

Module version 2.2.1.5
DLL version 2.2.1.5
OS Windows 11 24H2 (x64)
OS language German (de-DE)
PowerShell 5.1 (Desktop)
Execution context NT AUTHORITY\SYSTEM, local elevated session via RMM agent (not WinRM)
Update source Windows Update (default AU service), Microsoft Update also registered
WSUS none

Stack trace

Obtained via $_.Exception.StackTrace:

   at WUApiLib.IUpdate.get_IsDownloaded()
   at PSWindowsUpdate.GetWindowsUpdate.CoreProcessing()
   at PSWindowsUpdate.GetWindowsUpdate.ProcessRecord()
   at System.Management.Automation.CommandProcessor.ProcessRecord()

Exception details:

  • Type: System.ArgumentException
  • HResult: 0x80070057 (E_INVALIDARG)
  • ParamName: empty string
  • InnerException: none

The ArgumentException is the CLR's mapping of the COM E_INVALIDARG returned by the property getter — it is not a managed argument-validation failure inside the module.

Affected parameter sets

All three fail identically, with the same stack trace:

Install-WindowsUpdate -AcceptAll -MicrosoftUpdate
Install-WindowsUpdate -AcceptAll -WindowsUpdate
Install-WindowsUpdate -AcceptAll

This is worth stating explicitly because the error is frequently misattributed to -MicrosoftUpdate and the Microsoft Update service registration. On this machine the service was registered correctly the whole time:

Name                 ServiceID                            IsDefaultAUService
----                 ---------                            ------------------
Microsoft Update     7971f918-a847-4430-9279-4a52d1efe18d              False
DCat Flighting Prod  8b24b027-1dee-babb-9a95-3517dfb9c552              False
Windows Store (Prod) 855e8a7c-ecb4-4ca3-b045-1dfa50104289              False
Windows Update       9482f4b4-e343-43b6-b170-9a65bc822c77               True

Debug output

DEBUG: CmdletStart: get-windowsupdate
DEBUG: ParameterSetName: Default
DEBUG: Set pre search criteria: IsInstalled = 0
DEBUG: Set pre search criteria: IsHidden = 0
DEBUG: Search criteria is: IsInstalled = 0 and IsHidden = 0
DEBUG: <HOST>: Connecting...
DEBUG: Module version: 2.2.1.5
DEBUG: Dll version: 2.2.1.5
DEBUG: UpdateSessionObj mode: Local
DEBUG: ServiceManagerObj mode: Local
DEBUG: Try Default. Set source of updates to Windows Update
VERBOSE: <HOST>: Connecting to Windows Update server. Please wait...
VERBOSE: Found [17] Updates in pre search criteria
DEBUG: Security Intelligence-Update für Microsoft Defender Antivirus – KB2267602 (...)
DEBUG: Update was not filtered
DEBUG: Intel Driver Update (2.1.10501.1)
DEBUG: Update was not filtered
get-windowsupdate : Der Wert liegt außerhalb des erwarteten Bereichs.

Note that the search itself succeeds and returns 17 updates. The failure occurs during result processing, on the third item.

Minimal reproduction (module-independent)

The following demonstrates that the property getter itself throws, outside of PSWindowsUpdate:

$r = (New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher().Search("IsInstalled=0 and IsHidden=0")
for ($i = 0; $i -lt $r.Updates.Count; $i++) {
    $u = $r.Updates.Item($i)
    try {
        $v = $u.GetType().InvokeMember('IsDownloaded','GetProperty',$null,$u,$null)
        Write-Host ("[{0,2}] IsDownloaded={1,-5}  {2}" -f $i, $v, $u.Title)
    } catch {
        Write-Host ("[{0,2}] THROW  {1}" -f $i, $u.Title) -ForegroundColor Red
    }
}

Output on the affected machine:

[ 0] IsDownloaded=False  Security Intelligence-Update ... KB2267602 (Version 1.455.420.0)
[ 1] THROW  Intel Driver Update (2.1.10501.1)
[ 2] THROW  Samsung Firmware Driver Update (1.0.0.2)
[ 3] IsDownloaded=False  Logitech USB Driver Update (1.1.84.7627)
[ 4] IsDownloaded=False  Intel SoftwareComponent Driver Update (267.102.85.0)
...
[16] IsDownloaded=False  Lenovo System Driver Update (10.17.2606.3)

Exactly 2 of 17 update objects throw E_INVALIDARG; the remaining 15 return False.

Per-update isolation through the module confirms the same two:

foreach ($u in $r.Updates) {
    $id = $u.Identity.UpdateID
    try   { $null = Get-WindowsUpdate -Criteria "UpdateID='$id'" -ErrorAction Stop
            Write-Host "OK    $($u.Title)" }
    catch { Write-Host "FAIL  $($u.Title)" -ForegroundColor Red }
}

Note on reproducing this: $update.IsDownloaded in PowerShell will not reproduce the failure. The PowerShell COM adapter swallows failing property getters and silently returns $null, which is why this is easy to miss during diagnosis. InvokeMember (or any strongly-typed access, as in the compiled cmdlet) is required to observe the exception.

Root cause

The two failing updates were not special in their metadata. A full property dump comparing failing and non-failing driver updates showed no discriminating value — DeploymentAction, DriverClass, DeviceStatus, Categories, DriverVerDate and others all cross-cut both groups:

Intel (fail) Samsung (fail) Logitech (ok) Lenovo (ok)
DeploymentAction 1 1 1 4
DriverClass OtherHardware Firmware OtherHardware Firmware
DeviceStatus 25174026 0 25182218 25174026
Category Drivers Drivers Drivers Drivers

IsDownloaded is a state query against the local download cache (DataStore.edb under %WINDIR%\SoftwareDistribution). The failure was resolved by resetting that store:

Stop-Service wuauserv, bits, usosvc -Force
Rename-Item C:\Windows\SoftwareDistribution C:\Windows\SoftwareDistribution.bak
Start-Service bits, wuauserv

After the reset, all 17 updates return IsDownloaded = False, and Install-WindowsUpdate -AcceptAll completes normally.

So: inconsistent entries in the local Windows Update datastore cause IUpdate::get_IsDownloaded to return E_INVALIDARG for the affected update objects. This is an environmental condition the module cannot prevent — but it can handle it.

Impact

  1. A single unreadable property aborts the whole cmdlet. Nothing is returned, not even the updates that were processed successfully before the throw.
  2. The error message names neither the update nor the property, making self-diagnosis effectively impossible for the average user. This is, I believe, why this failure has such a long tail of unresolved reports.
  3. -NotCategory 'Drivers' is not a viable workaround, since it is applied after the update object is constructed — i.e. after the throw.
  4. The only working workarounds move the filter into the pre-search criteria, which means excluding entire update classes:
    Install-WindowsUpdate -UpdateType Software -AcceptAll
    Install-WindowsUpdate -Criteria "IsInstalled = 0 and IsHidden = 0 and Type='Software'" -AcceptAll
    Neither is acceptable as a general solution, because it also skips driver and firmware updates that are perfectly healthy.

Suggested fix

Guard the property read in CoreProcessing() and degrade gracefully instead of terminating. Sketch:

private static T SafeGet<T>(Func<T> getter, T fallback, string propertyName,
                            string updateTitle, Action<string> warn)
{
    try { return getter(); }
    catch (Exception ex)
    {
        warn($"Could not read '{propertyName}' for update '{updateTitle}' " +
             $"(HRESULT 0x{ex.HResult:X8}). This usually indicates a corrupted Windows Update " +
             $"datastore; consider resetting %WINDIR%\\SoftwareDistribution. Update skipped.");
        return fallback;
    }
}

Used at the call site:

bool isDownloaded = SafeGet(() => update.IsDownloaded, false,
                            nameof(update.IsDownloaded), title, WriteWarning);

Two points worth considering beyond the immediate fix:

  • Skip rather than substitute. For IsDownloaded a false fallback is harmless for Get-WindowsUpdate, but for Install-WindowsUpdate it would cause a download attempt against an update whose cache state is unknown. Excluding the update from the result set and emitting a warning is probably the safer default.
  • IsDownloaded is unlikely to be the only affected getter. If the datastore is inconsistent, any state-dependent property on IUpdate may throw. A single defensive accessor applied consistently across the property reads in CoreProcessing() would be more robust than patching this one call site.

Even without any behavioural change, simply including the update title and property name in the error message would be a substantial improvement over the current output.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions