Skip to content

Using String.Format crashes inside pythonnet #1642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
sekkit opened this issue Dec 21, 2021 · 15 comments · Fixed by #1710 or #1711
Closed

Using String.Format crashes inside pythonnet #1642

sekkit opened this issue Dec 21, 2021 · 15 comments · Fixed by #1710 or #1711
Assignees
Labels
Milestone

Comments

@sekkit
Copy link

sekkit commented Dec 21, 2021

Environment

  • Pythonnet version: 3.x
  • Python version: 3.8.10
  • Operating System: Windows 11 64bit
  • .NET Runtime: net6.0

Details

How to reproduce(with project Console.vsproj): @filmor

PythonEngine.Initialize();
PythonEngine.Exec("from System import String\nString.Format('{0},{1}', 1, 2)"); // crashes here
int i = Runtime.Py_Main(cmd.Length, cmd);
PythonEngine.Shutdown();
@filmor
Copy link
Member

filmor commented Dec 22, 2021

I can't reproduce this in the current alpha. Please provide the exact commit of pythonnet that you are running against and the exact project that reproduces the error. Also, please do not open another issue (this is the same as #1641 already). Edit this one, upload things here and reopen the issue afterwards.

@filmor filmor closed this as completed Dec 22, 2021
@sekkit
Copy link
Author

sekkit commented Dec 22, 2021

I can't reproduce this in the current alpha. Please provide the exact commit of pythonnet that you are running against and the exact project that reproduces the error. Also, please do not open another issue (this is the same as #1641 already). Edit this one, upload things here and reopen the issue afterwards.

im using the master branch
here is the project @filmor
pythonnet-3.0.0-a2 (2).zip

PS: another branch https://github.com/QuantConnect/pythonnet works fine with String.Format

@sekkit
Copy link
Author

sekkit commented Dec 23, 2021

System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.'
[External Code]

Python.Runtime.dll!Python.Runtime.Runtime.PyErr_Occurred() Line 1853 C#
Python.Runtime.dll!Python.Runtime.Exceptions.ErrorOccurred() Line 315 C#
Python.Runtime.dll!Python.Runtime.Runtime.AssertNoErorSet() Line 1130 C#
Python.Runtime.dll!Python.Runtime.Runtime.PyObject_Str(Python.Runtime.BorrowedReference pointer) Line 1122 C#
Python.Runtime.dll!Python.Runtime.PyObject.ToString() Line 1054 C#
[External Code]
Python.Runtime.dll!Python.Runtime.MethodBinder.Invoke(Python.Runtime.BorrowedReference inst, Python.Runtime.BorrowedReference args, Python.Runtime.BorrowedReference kw, System.Reflection.MethodBase info, System.Reflection.MethodInfo[] methodinfo) Line 919 C#
Python.Runtime.dll!Python.Runtime.MethodObject.Invoke(Python.Runtime.BorrowedReference target, Python.Runtime.BorrowedReference args, Python.Runtime.BorrowedReference kw, System.Reflection.MethodBase info) Line 68 C#
Python.Runtime.dll!Python.Runtime.MethodBinding.tp_call(Python.Runtime.BorrowedReference ob, Python.Runtime.BorrowedReference args, Python.Runtime.BorrowedReference kw) Line 238 C#
[External Code]
Python.Runtime.dll!Python.Runtime.Runtime.PyRun_String(string code, Python.Runtime.RunFlagType st, Python.Runtime.BorrowedReference globals, Python.Runtime.BorrowedReference locals) Line 941 C#
Python.Runtime.dll!Python.Runtime.PythonEngine.RunString(string code, Python.Runtime.BorrowedReference globals, Python.Runtime.BorrowedReference locals, Python.Runtime.RunFlagType flag) Line 670 C#
Python.Runtime.dll!Python.Runtime.PythonEngine.Exec(string code, Python.Runtime.PyDict globals, Python.Runtime.PyObject locals) Line 578 C#
nPython.exe!Python.Runtime.PythonConsole.Main(string[] args) Line 36 C#

@lostmsu lostmsu reopened this Dec 23, 2021
@lostmsu
Copy link
Member

lostmsu commented Dec 23, 2021

I can confirm this reproduces. I believe the reason is that we no longer automatically convert PyInt to System.Int32, because it is an unsafe conversion due to potential overflows.

Instead, you should do String.Format('{0},{1}', str(1), str(2)) or

from System import Int32, String
String.Format('{0},{1}', Int32(1), Int32(2))

In general though this was always a problem with any Python objects passed to methods like this. String.Format does not acquire GIL when calling ToString on the arguments. E.g. in 2.x or QuantConnect fork this would probably crash with a similar error: String.Format('{0},{1}', str(1), { 'a': 'b' }).

In my TensorFlow binding for that reason I wrap all PyObject instances into a nearly identical type, that delegates all its members to PyObject, but acquires GIL for every method call.

However, I do not think it is a good approach for the core library. If somebody wants to autoacquire GIL on all Python object accesses, we can provide a guidance on how to set it up.

@filmor thoughts?

P.S. keeping open until we make it less surprising, or at least document it.

@filmor
Copy link
Member

filmor commented Dec 23, 2021

Hmm, this is not good. It should not be possible to crash the interpreter with a simple thing like this. We should enforce safe operation of all .NET methods that PyObject implements (i.e. either work properly or throw), so all methods of object and DynamicObject, and maybe IDisposable and ISerializable.

@filmor filmor added this to the 3.0.0 milestone Dec 23, 2021
@lostmsu
Copy link
Member

lostmsu commented Dec 23, 2021

@filmor anything will crash the interpreter without acquiring GIL.

@filmor
Copy link
Member

filmor commented Dec 23, 2021

I am well aware of that :). I just think that it shouldn't be possible to crash the interpreter (easily) from within Python.

@lostmsu
Copy link
Member

lostmsu commented Dec 23, 2021

Well, checking if we have GIL is pretty expensive (would slow down all members in PyObject), and not releasing it upon calling .NET methods will prevent multithreaded code from progressing Python threads while .NET is running.

Maybe we could make calling .NET methods safe from Python by default, and have clr.no_gil wrapper for unsafe calls. E.g.

String.Format('{0},{1}', 1, 2) # this would be OK
clr.no_gil(String.Format)('{0},{1}', 1, 2) # would crash as in the current issue

But frankly I think we should just keep the current behavior, or add clr.with_gil so that clr.with_gil(String.Format)('{0},{1}', 1, 2) would not crash.

@sekkit
Copy link
Author

sekkit commented Dec 24, 2021

Any conclusions with this issue? Are we going to leave it as it is or alter the usage of such case?

@filmor
Copy link
Member

filmor commented Dec 24, 2021

I definitely want to have this fixed before we consider releasing an rc, but I won't think about this over the holidays :)

@lostmsu
Copy link
Member

lostmsu commented Dec 24, 2021

@sekkit regardless of what we decide, you should be using one of the samples I provided above depending on who's formatting abilities you need, .NET (use Int32(val), then the arguments are .NET values and the entire call is .NET business) or Python (use str(val), then the arguments are converted to strings by Python before being passed to String.Format), they are guaranteed to work in any case.

@lostmsu
Copy link
Member

lostmsu commented Jan 6, 2022

One option is to have a debug build that would fail an assertion or raise an exception upon any attempt to call PyObject methods without GIL. In NuGet it could be sufficient to suffix version with -debug I think. Not sure about PyPi.

@filmor
Copy link
Member

filmor commented Jan 7, 2022

I don't think that would be a good idea. There is no need to do this in a separate build, we can pass the respective information dynamically. Checking a readonly bool should be something that the JIT compiler should be able to optimise. I'll create a draft PR where we can discuss the exact behaviour further. My goal would be that (by default, when embedded in Python), all overrides of "native" .NET classes in PyObject check for the GIL and either acquire it or throw.

@filmor
Copy link
Member

filmor commented Mar 3, 2022

We decided that we are going to GIL-protect all overridden object and DynamicObject methods.

@lostmsu lostmsu self-assigned this Mar 3, 2022
@lostmsu
Copy link
Member

lostmsu commented Mar 3, 2022

@sekkit @filmor FYI, the workaround we are planning is going to be somewhat misleading. In your example if you added integer format specifiers, and still used number literals in Python, you would get your format specifiers ignored.

UPD. NVM, I will just add IFormattable implementation to PyInt.

lostmsu added a commit to losttech/pythonnet that referenced this issue Mar 3, 2022
lostmsu added a commit to losttech/pythonnet that referenced this issue Mar 3, 2022
lostmsu added a commit to losttech/pythonnet that referenced this issue Mar 3, 2022
lostmsu added a commit to losttech/pythonnet that referenced this issue Mar 3, 2022
lostmsu added a commit to losttech/pythonnet that referenced this issue Mar 3, 2022
lostmsu added a commit to losttech/pythonnet that referenced this issue Mar 3, 2022
filmor pushed a commit that referenced this issue Apr 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment