Skip to content

MethodBinder.CheckMethodArgumentsMatch throws unhandled IndexOutOfRangeException on empty **kwargs call to an overloaded method #132

Description

@AlexCatarino

Summary

Calling an overloaded .NET method from Python with an empty **kwargs dict
(e.g. obj.Method(arg, **{})) throws an unhandled
System.IndexOutOfRangeException inside
MethodBinder.CheckMethodArgumentsMatch instead of binding normally (as the
equivalent positional call obj.Method(arg) does) or raising a clean
TypeError.

Because the exception is an unhandled native IndexOutOfRangeException, it
tears down the host process rather than surfacing as a catchable Python error.
In our case (the LEAN engine) this crashes the algorithm during
Initialize(), before any user output.

Environment

  • QuantConnect/pythonnet fork (bundled with LEAN engine v2.5.0.0, 64-bit)
  • Python 3.11.14 (GCC 11.2.0), Linux
  • .NET runtime as shipped with the above

(Please confirm the exact pythonnet commit on the fork; the crash is in
MethodBinder.CheckMethodArgumentsMatch / MethodBinder.Bind.)

Reproduction (confirmed, via LEAN QCAlgorithm.get_parameter)

QCAlgorithm.GetParameter is overloaded:

string GetParameter(string name);
string GetParameter(string name, string defaultValue);
int    GetParameter(string name, int    defaultValue);
double GetParameter(string name, double defaultValue);

A single-file algorithm that wraps get_parameter and forwards the call with
*args, **kwargs (a common Python decorator/monkeypatch idiom) crashes on the
first invocation:

# region imports
from AlgorithmImports import *
# endregion


class MultiStrategyAlgorithm(QCAlgorithm):
    def initialize(self):
        # Wrap the C# get_parameter, then forward positionally + **kwargs.
        # Every caller invokes get_parameter(name) with a single positional
        # arg, so kwargs is ALWAYS an empty dict here -- yet forwarding it
        # still crashes the binder.
        original = self.get_parameter

        def recording_get_parameter(name, *args, **kwargs):
            return original(name, *args, **kwargs)   # <-- crashes

        self.get_parameter = recording_get_parameter

        # First call through the wrapper -> System.IndexOutOfRangeException
        self.get_parameter("probe-network")
  • original(name, *args, **kwargs) with an empty kwargscrash
  • original(name) (no **kwargs) → works
  • self.get_parameter("probe-network") called directly → works

So the trigger is purely the presence of a (borrowed, empty) keyword-args dict
on the call, which routes binding through the kwargDict code path.

Distilled trigger (pure pythonnet — please confirm)

The above reduces to: any overloaded method whose overloads differ in
parameter count, invoked with an empty kwargs mapping.

public class Overloaded
{
    public string Get(string name)               => name;
    public string Get(string name, string def)   => def;
    public int    Get(string name, int def)      => def;
}
o = Overloaded()
o.Get("x")         # OK
o.Get("x", **{})   # System.IndexOutOfRangeException in CheckMethodArgumentsMatch

Actual behavior — traceback

Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Python.Runtime.MethodBinder.CheckMethodArgumentsMatch(Int32 clrArgCount, Int32 pyArgCount, Dictionary`2 kwargDict, ParameterInfo[] parameterInfo, String[] parameterNames, Boolean& paramsArray, ArrayList& defaultArgList)
   at Python.Runtime.MethodBinder.Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info)
   at Python.Runtime.MethodBinder.Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo)
   at Python.Runtime.MethodBinder.Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info)
   at Python.Runtime.MethodObject.Invoke(BorrowedReference target, BorrowedReference args, BorrowedReference kw, MethodBase info)
   at Python.Runtime.MethodBinding.tp_call(BorrowedReference ob, BorrowedReference args, BorrowedReference kw)

Expected behavior

Forwarding an empty **kwargs should be equivalent to the positional-only call:
the method should bind and return normally. At minimum, an unresolvable
kwarg/overload combination should raise a Python-catchable exception (e.g.
TypeError/ArgumentException), never an unhandled
IndexOutOfRangeException that crashes the host.

Analysis / pointer

The fault is in MethodBinder.CheckMethodArgumentsMatch (called from
Bind) when a non-null kwargDict is present. It appears to index
parameterInfo / parameterNames out of range while reconciling positional
args, the kwargs dict, and overloads of differing arity — even when the kwargs
dict is empty. An empty keyword mapping should be treated as no keyword
arguments (fast path), and the bounds arithmetic on the kwargDict path should
be guarded.

Notes

  • Found via the LEAN engine; the minimal single-file reproduction above is
    available as a standalone project.
  • Impact is elevated because the exception is unhandled at the native boundary:
    it does not propagate as a Python exception, so callers cannot try/except
    it, and long-running hosts crash outright.

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions