This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: ctypes ignores when a DLL function is called with too many arguments
Type: behavior Stage: needs patch
Components: ctypes, Documentation, Windows Versions: Python 3.9, Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: docs@python Nosy List: docs@python, eryksun, paul.moore, smernst, steve.dower, tim.golden, zach.ware
Priority: normal Keywords:

Created on 2019-09-23 14:35 by smernst, last changed 2022-04-11 14:59 by admin.

Messages (4)
msg353021 - (view) Author: Sebastian Ernst (smernst) Date: 2019-09-23 14:35
A c-function with the following signature ...

```C
int16_t __stdcall __declspec(dllimport) square_int(
	int16_t a
	);
```

... is being called with ctypes:

```python
def test_error_callargs_unconfigured_too_many_args():

	dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll')
	square_int = dll.square_int

	with pytest.raises(ValueError):
		a = square_int(1, 2, 3)
```

Expected result: If the function is called with too many (positional) arguments (in the example 3 instead of 1), a `ValueError` should be raised. This is the case for at least CPython 3.4 to 3.7.

Actual result: "Nothing", i.e. no exception. The described test "fails". The function is called without an error - with CPython 3.8.0b4.

If this behavior is intended, is has not been (as far as I can tell) documented.
msg353036 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2019-09-23 16:58
According to the docs, raising ValueError in this case has been a deprecated feature since 3.6.2, and the ability to do so no longer exists in 3.8. The documentation needs to be updated to reflect the new behavior.

https://docs.python.org/3.8/library/ctypes.html#calling-functions

In issue 35947, the customized libffi_msvc was replaced by standard libffi. ffi_call() in libffi_msvc returned a stack cleanup delta. This allowed raising ValueError in _call_function_pointer if the delta value was non-zero for an x86 32-bit stdcall function (callee cleanup). A positive delta implied too many arguments, and a negative delta implied too few arguments. The standard libffi ffi_call() has no return value, so the code for raising ValueError was removed.

---

On a related note, PyCFuncPtr_call can be relaxed to allow the argument count to exceed the length of argtypes for x64 stdcall (but not x86). The 64-bit calling convention is the same for cdecl, stdcall, fastcall, and thiscall. It uses caller cleanup, like x86 cdecl, so we can skip raising TypeError in this case, just like we already do for cdecl.
msg353069 - (view) Author: Sebastian Ernst (smernst) Date: 2019-09-24 10:42
Thanks a lot for the clarification, Eryk. I did not notice that it was deprecated behavior.

For the "too many arguments" case I guess this is not an issue. However, just for the symmetry of things, I also looked at calling a function with TOO FEW arguments. 

```C
int16_t __stdcall __declspec(dllimport) mul_ints(
	int16_t a,
	int16_t b
	)
{
	return a * b;
}
```

```python
def test_error_callargs_unconfigured_too_few_args():

	dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll')
	mul_ints = dll.mul_ints

	with pytest.raises(ValueError):
		a = mul_ints(7)
```

As expected after your explanation, also no error in CPython 3.8 (i.e. the test fails, while is passes on CPython <= 3.7). If I run this manually, I even get a "result":

```python
>>> dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll')
>>> mul_ints = dll.mul_ints
>>> a = mul_ints(7)
>>> a
445564 # !
>>> 445564/7 # Just looking at where this result is coming from ...
63652.0
```

Re-starting Python (3.8) and (intentionally) misconfiguring the function interestingly also does not change the result:

```python
>>> dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll')
>>> mul_ints = dll.mul_ints
>>> mul_ints.argtypes = (ctypes.c_int16,)
>>> a = mul_ints(7)                                      
>>> a
445564 # Apparently, this is deterministic?!?
```

Just out of curiosity, where is this data coming from?

This ONLY way to prevent things like this to happen is to actually correctly configure the function:

```python
>>> dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll')
>>> mul_ints = dll.mul_ints
>>> mul_ints.argtypes = (ctypes.c_int16, ctypes.c_int16)
>>> mul_ints(7)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: this function takes 2 arguments (1 given)
```

This should very CLEARLY be mentioned in the documentation ...
msg353100 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2019-09-24 16:56
> Just out of curiosity, where is this data coming from?

In general it's just random data on the stack. It's not worth investigating where this particular value comes from. It could be the low word of the address of a stack-allocated buffer, such as the buffer where ffi_call writes the return value.

> This ONLY way to prevent things like this to happen is to actually 
> correctly configure the function:
> ...
> >>> mul_ints.argtypes = (ctypes.c_int16, ctypes.c_int16)

Yes, the docs should explain that TypeError is raised for the wrong number of arguments when argtypes is assigned, and it should be noted that cdecl (CDLL) allows passing more (but not fewer) arguments than what argtypes defines. As I mentioned previously, we could extend this cdecl behavior to stdcall (WinDLL) for the x64 architecture, since it's caller cleanup like cdecl.
History
Date User Action Args
2022-04-11 14:59:20adminsetgithub: 82439
2019-09-24 16:56:54eryksunsetmessages: + msg353100
2019-09-24 10:42:06smernstsetmessages: + msg353069
2019-09-23 16:58:33eryksunsetassignee: docs@python
components: + Documentation, Windows
versions: + Python 3.9
nosy: + paul.moore, tim.golden, eryksun, docs@python, zach.ware, steve.dower

messages: + msg353036
stage: needs patch
2019-09-23 14:35:01smernstcreate