Skip to content

Commit 9b107f4

Browse files
authored
Merge branch 'master' into just
2 parents 45a7281 + deca529 commit 9b107f4

6 files changed

Lines changed: 99 additions & 8 deletions

File tree

‎.github/workflows/main.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ jobs:
5252

5353
env:
5454
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
55+
# Apply to every Mono host, including pytest and the .NET Python test runner.
56+
MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466
5557
steps:
5658
- name: Checkout code
5759
uses: actions/checkout@v7
@@ -87,8 +89,6 @@ jobs:
8789
- name: Embedding tests (Mono/.NET Framework)
8890
if: always()
8991
run: just test-embed-mono ${{ matrix.os.platform }}
90-
env:
91-
MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466
9292

9393
- name: Embedding tests (.NET Core)
9494
if: always()

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
99

1010
### Added
1111

12+
### Changed
13+
14+
### Fixed
15+
16+
17+
## 3.2.0 - 2026-09-29
18+
19+
### Added
20+
1221
- Support for Python 3.15 (#2729)
1322
- Support for free-threaded Python 3.14 (`Py_GIL_DISABLED`) (#2721)
1423
- Threading documentation covering GIL ownership, managed threads, and
@@ -28,6 +37,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
2837
- Fix method binding initialization when methods are first accessed (#2721)
2938
- Fix wheel and NuGet preview package builds
3039
- Python exception raised on conversion failure (#2741)
40+
- Introduce additional pre-checks in `PyBuffer_GetPointer` to prevent memory
41+
corruption (#2745)
3142

3243
## 3.1.0 - 2026-05-23
3344

‎doc/source/threading.rst‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ Forgetting this is the most common pythonnet threading bug. Symptoms range
9393
from immediate segfaults to subtle refcount corruption that crashes much
9494
later.
9595

96+
Mono thread suspension
97+
----------------------
98+
99+
On Linux and macOS, Mono's cooperative/hybrid thread suspension can hang
100+
when Python and managed threads interact. If affected, set
101+
``MONO_THREADS_SUSPEND=preemptive`` before starting the process, as described
102+
in `mono/mono#21466 <https://github.com/mono/mono/issues/21466>`_.
103+
CI uses this workaround for both embedding and Python-hosted tests.
104+
105+
To run the Mono Python tests with per-test output and a Python thread
106+
stack dump if a test takes longer than 60 seconds::
107+
108+
MONO_THREADS_SUSPEND=preemptive uv run pytest --runtime mono -v -o faulthandler_timeout=60
109+
96110
Reference counting and finalizers
97111
---------------------------------
98112

‎src/embed_tests/TestPyBuffer.cs‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
using System.Runtime.CompilerServices;
33
using System.Text;
44
using System.Threading;
5+
56
using NUnit.Framework;
7+
68
using Python.Runtime;
79
using Python.Runtime.Codecs;
810

9-
namespace Python.EmbeddingTest {
11+
namespace Python.EmbeddingTest
12+
{
1013
class TestPyBuffer
1114
{
1215
[OneTimeSetUp]
@@ -58,13 +61,49 @@ public void TestBufferRead()
5861
}
5962

6063
string result = new UTF8Encoding().GetString(managedArray);
61-
Assert.That(result == " " + bufferTestString.Substring(1), Is.True);
64+
Assert.That(result, Is.EqualTo($" {bufferTestString.Substring(1)}"));
65+
}
66+
67+
[Test]
68+
public void GetPointer()
69+
{
70+
// create 3D NumPy array
71+
int[] shape = [10, 10, 10];
72+
using PyObject ndArray = np.zeros(shape);
73+
74+
using PyBuffer buf = ndArray.GetBuffer(PyBUF.STRIDES);
75+
76+
var ptr = buf.GetPointer([0, 0, 0]);
77+
78+
Assert.That(ptr, Is.Not.EqualTo(IntPtr.Zero));
79+
80+
var ptrEnd = buf.GetPointer([9, 9, 9]);
81+
Assert.That(ptrEnd, Is.Not.EqualTo(IntPtr.Zero));
82+
83+
Assert.Throws<ArgumentNullException>(() => buf.GetPointer(null!));
84+
85+
Assert.Throws<ArgumentOutOfRangeException>(() => buf.GetPointer([0, 0]));
86+
Assert.Throws<ArgumentOutOfRangeException>(() => buf.GetPointer([0, 0, 0, 0]));
87+
88+
Assert.Throws<ArgumentOutOfRangeException>(() => buf.GetPointer([-1, 0, 0]));
89+
Assert.Throws<ArgumentOutOfRangeException>(() => buf.GetPointer([0, -1, 0]));
90+
Assert.Throws<ArgumentOutOfRangeException>(() => buf.GetPointer([0, 0, -1]));
91+
92+
Assert.Throws<ArgumentOutOfRangeException>(() => buf.GetPointer([10, 0, 0]));
93+
Assert.Throws<ArgumentOutOfRangeException>(() => buf.GetPointer([0, 10, 0]));
94+
Assert.Throws<ArgumentOutOfRangeException>(() => buf.GetPointer([0, 0, 10]));
95+
96+
using PyBuffer buf2 = ndArray.GetBuffer(PyBUF.ND);
97+
98+
Assert.Throws<InvalidOperationException>(
99+
() => buf2.GetPointer([0, 0, 0])
100+
);
62101
}
63102

64103
[Test]
65104
public void ArrayHasBuffer()
66105
{
67-
var array = new[,] {{1, 2}, {3,4}};
106+
var array = new[,] { { 1, 2 }, { 3, 4 } };
68107
var memoryView = PythonEngine.Eval("memoryview");
69108
var mem = memoryView.Invoke(array.ToPython());
70109
Assert.That(mem[(0, 0).ToPython()].As<int>(), Is.EqualTo(1));
@@ -113,7 +152,7 @@ public void Finalization()
113152
[Test]
114153
public void MultidimensionalNumPyArray()
115154
{
116-
var ndarray = np.arange(24).reshape(1,2,3,4).T;
155+
var ndarray = np.arange(24).reshape(1, 2, 3, 4).T;
117156
PyObject ndim = ndarray.ndim;
118157
PyObject shape = ndarray.shape;
119158
PyObject strides = ndarray.strides;

‎src/runtime/PythonTypes/PyBuffer.cs‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,37 @@ public bool IsContiguous(BufferOrderStyle order)
110110
/// </summary>
111111
public IntPtr GetPointer(long[] indices)
112112
{
113-
if (indices is null) throw new ArgumentNullException(nameof(indices));
113+
if (indices is null)
114+
throw new ArgumentNullException(nameof(indices));
115+
116+
if (Strides is null)
117+
throw new InvalidOperationException($"{nameof(GetPointer)} requires {nameof(Strides)} to be filled");
118+
119+
if (indices.Length != Dimensions)
120+
throw new ArgumentOutOfRangeException($"{nameof(indices)} must be exactly {Dimensions} long");
121+
122+
if (Dimensions == 0 && Shape is null)
123+
throw new InvalidOperationException($"{nameof(GetPointer)} requires {nameof(Shape)} to be filled for dimensional buffers");
124+
114125
ThrowIfDisposed();
126+
115127
if (Runtime.PyVersion < new Version(3, 7))
116128
throw new NotSupportedException("GetPointer requires at least Python 3.7");
129+
130+
var indicesArr = new nint[Dimensions];
131+
132+
for (var i = 0; i < Dimensions; i++)
133+
{
134+
indicesArr[i] = checked((nint)indices[i]);
135+
136+
if (indicesArr[i] < 0 || indicesArr[i] >= Shape![i])
137+
{
138+
throw new ArgumentOutOfRangeException(
139+
$"Index {i} has a value of {indicesArr[i]} not in [0, {Shape![i]})"
140+
);
141+
}
142+
}
143+
117144
return Runtime.PyBuffer_GetPointer(ref _view, indices.Select(x => checked((nint)x)).ToArray());
118145
}
119146

‎version.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.2.0-rc2
1+
3.3.0-dev

0 commit comments

Comments
 (0)