Skip to content

TaskDialogPage.GetBoundButtonByID throws IndexOutOfRangeException when TaskDialogIndirect returns unexpected button ID #14831

Description

@ABA2396

.NET version

.NET 10.0 (observed in an app targeting net10.0-windows10.0.17763.0)

Did it work in .NET Framework?

Yes

Did it work in any of the earlier releases of .NET Core or .NET 5+?

No. TaskDialog was introduced in .NET Core 3.1 / .NET 5 (it does not exist in .NET Framework).

Issue description

TaskDialog.ShowDialog throws an unhandled IndexOutOfRangeException from TaskDialogPage.GetBoundButtonByID when the native TaskDialogIndirect API returns a button ID that falls in the custom-button range (≥ 100) but the page has no custom buttons configured.

Stack trace

System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Windows.Forms.TaskDialogPage.GetBoundButtonByID(Int32 buttonID)
   at System.Windows.Forms.TaskDialog.ShowDialogInternal(IntPtr hwndOwner, TaskDialogPage page, TaskDialogStartupLocation startupLocation)
   at System.Windows.Forms.TaskDialog.ShowDialog(IntPtr hwndOwner, TaskDialogPage page, TaskDialogStartupLocation startupLocation)

Root cause

In TaskDialogPage.GetBoundButtonByID (source: src/System.Windows.Forms/System/Windows/Forms/Dialogs/TaskDialog/TaskDialogPage.cs):

internal TaskDialogButton? GetBoundButtonByID(int buttonID)
{
    if (BoundDialog is null)
    {
        throw new InvalidOperationException();
    }

    if (buttonID == 0)
    {
        return null;
    }

    // Check if the button is part of the custom buttons.
    if (buttonID >= CustomButtonStartID)  // CustomButtonStartID = 100
    {
        return _boundCustomButtons![buttonID - CustomButtonStartID];  // ← No bounds check!
    }
    else
    {
        _boundStandardButtonsByID!.TryGetValue(buttonID, out TaskDialogButton? button);
        return button;
    }
}

When buttonID >= 100 but _boundCustomButtons is empty (because no custom buttons were configured), the array access _boundCustomButtons[buttonID - 100] throws IndexOutOfRangeException.

The caller in TaskDialog.ShowDialogInternal already anticipates this method possibly returning null and handles it:

return _boundPage.GetBoundButtonByID(resultButtonID) ??
    CreatePlaceholderButton((TaskDialogResult)resultButtonID);

But the exception prevents the null-coalescing fallback from ever executing.

Why might TaskDialogIndirect return an unexpected ID?

The .NET source already acknowledges that TaskDialogIndirect can return unexpected button IDs in abnormal close scenarios:

"in some cases when the dialog is closed abnormally (e.g. when closing the main window while a modeless task dialog is displayed), the dialog returns IDCANCEL (2) without priorly raising the TDN_BUTTON_CLICKED notification."

In our case, the returned ID was ≥ 100 (custom-button range) rather than a standard button ID, and it was returned for a dialog that only had a standard OK button configured. We encountered this while showing a TaskDialog to warn the user about a third-party injected DLL (AudioDevProps2.dll from Nahimic / A-Volute) detected in the process. We cannot confirm whether the injected DLL is the direct cause of the unexpected return value, but regardless of the trigger, GetBoundButtonByID should not throw on an out-of-range ID — it should return null, which the caller already handles.

Same bug in GetBoundRadioButtonByID

GetBoundRadioButtonByID in the same file has an identical unguarded access:

internal TaskDialogRadioButton? GetBoundRadioButtonByID(int buttonID)
{
    if (BoundDialog is null)
    {
        throw new InvalidOperationException();
    }

    return buttonID == 0 ? null : _radioButtons[buttonID - RadioButtonStartID];  // ← No bounds check
}

Steps to reproduce

Note: I could not reproduce this on a clean machine. The crash was observed in a real-world scenario where the dialog was closed abnormally (see below). However, the underlying bug — an unguarded array access — can be verified directly from the source code.

Our scenario

We encountered this in an open-source WPF app (MaaAssistantArknights). Our app detects third-party DLLs that get injected into the process (e.g. audio OSD software like Nahimic), because these injected DLLs are known to break WPF rendering and cause crashes. To warn the user about this, we deliberately chose to use TaskDialog (WinForms) instead of a WPF dialog — precisely because the injected DLLs we are warning about already break WPF rendering, so we cannot rely on WPF to display the warning itself.

The crash is intermittent — sometimes the dialog returns normally, sometimes it crashes. When it does crash, the exception propagates up and terminates the app.

Our calling code

var page = new TaskDialogPage
{
    Caption = "MAA",
    Heading = "Warning Heading",
    Text = warningText,
    Icon = TaskDialogIcon.Warning,
    Buttons = { TaskDialogButton.OK },   // Only a standard button (ID = 1), no custom buttons
    SizeToContent = true,
    Verification = new()
    {
        Text = "Don't show again",
        Checked = false,
    },
};

var result = TaskDialog.ShowDialog(owner, page);

The dialog only has a standard OK button (ID = 1), so normally TaskDialogIndirect should return 1. In our case, it returned an ID ≥ 100 (custom-button range) instead, and GetBoundButtonByID tried to index into the empty _boundCustomButtons array and threw.

The injected DLL in question was AudioDevProps2.dll from Nahimic / A-Volute (C:\ProgramData\A-Volute\A-Volute.Nahimic\Modules\Scheduled\x64\AudioDevProps2.dll). We cannot confirm whether the injected DLL is the direct cause of the unexpected return value — the crash may also be triggered by other abnormal close paths.

Log excerpt

We received reports from two different users. Both had the same injected DLL (AudioDevProps2.dll from Nahimic) detected in their processes.

In the first user's log, the app crashed once at startup, then on the next launch the dialog was shown and dismissed normally, and our code successfully logged the detected module:

[2026-07-31 12:08:22.071][FTL][Bootstrapper]           <2> Unhandled exception occurred
System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Windows.Forms.TaskDialogPage.GetBoundButtonByID(Int32 buttonID)
   at System.Windows.Forms.TaskDialog.ShowDialogInternal(IntPtr hwndOwner, TaskDialogPage page, TaskDialogStartupLocation startupLocation)
   at System.Windows.Forms.TaskDialog.ShowDialog(IntPtr hwndOwner, TaskDialogPage page, TaskDialogStartupLocation startupLocation)
   at MaaWpfGui.Utilities.BadModules.CheckAndWarnBadInjectedModules()
   at MaaWpfGui.Main.Bootstrapper.OnLaunch()
   ...

[2026-07-31 12:31:53.053][WRN][BadModules]             <2> Detected bad injected modules:
C:\ProgramData\A-Volute\A-Volute.Nahimic\Modules\Scheduled\x64\AudioDevProps2.dll

In the second user's log, the crash occurred on every launch (4 times), and the app never started successfully:

[2026-07-31 13:34:24.533][FTL][Bootstrapper]           <2> Unhandled exception occurred
System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Windows.Forms.TaskDialogPage.GetBoundButtonByID(Int32 buttonID)
   ...

[2026-07-31 13:37:08.930][FTL][Bootstrapper]           <2> Unhandled exception occurred
System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Windows.Forms.TaskDialogPage.GetBoundButtonByID(Int32 buttonID)
   ...

[2026-07-31 13:37:34.020][FTL][Bootstrapper]           <2> Unhandled exception occurred
System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Windows.Forms.TaskDialogPage.GetBoundButtonByID(Int32 buttonID)
   ...

[2026-07-31 13:44:18.728][FTL][Bootstrapper]           <2> Unhandled exception occurred
System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Windows.Forms.TaskDialogPage.GetBoundButtonByID(Int32 buttonID)
   ...

The crash is intermittent — sometimes the dialog returns normally, sometimes it crashes. We could not determine what makes the difference.

Verification without reproduction

The bug can be confirmed by inspecting TaskDialogPage.GetBoundButtonByID: when _boundCustomButtons is empty and buttonID >= 100, the expression _boundCustomButtons[buttonID - CustomButtonStartID] will always throw IndexOutOfRangeException. There is no bounds check or fallback to null, even though the caller (ShowDialogInternal) already handles null via ?? CreatePlaceholderButton(...).

Metadata

Metadata

Assignees

Labels

untriagedThe team needs to look at this issue in the next triage

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions