Skip to content

Commit 12d0372

Browse files
[nt-0] fix: Protect against null pointers & disposed objects in C# wrapper (#786)
* [nt-0] fix: Protect against null pointers Attempt to prevent the class of native level crash caused by accessing invalid pointers. * `Dispose()` sets the internal pointer to zero * Changes `_ptr` to a property * Accessing _ptr when it's `IntPtr.Zero` throws a NullReferenceException * Prevent modification of the underlying pointer for `NativeClassWrapper` * [nt-0] docs: xmldocs for NativeClassWrapper
1 parent d21278e commit 12d0372

File tree

6 files changed

+112
-15
lines changed

6 files changed

+112
-15
lines changed

Library/CSharpWrapper/src/NativeClassWrapper.cs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,92 @@ internal NativePointer(IntPtr pointer, byte ownsOwnData)
3737
public static NativePointer Zero => new NativePointer(IntPtr.Zero, 0);
3838
}
3939

40+
/// <summary>
41+
/// Base class for all CSP objects.
42+
/// Provides base memory management for derived interop types.
43+
/// </summary>
4044
public class NativeClassWrapper
4145
{
42-
internal IntPtr _ptr;
46+
/// <summary>
47+
/// The native C pointer to the object for use with P/Invoke methods.
48+
/// </summary>
49+
/// <remarks>
50+
/// This must be accessed through <see cref="_ptr"/> to avoid passing `nullptr` to
51+
/// member functions in native code.
52+
/// </remarks>
53+
private IntPtr _ptrValue = IntPtr.Zero;
54+
55+
/// <summary>
56+
/// Cached value of <see cref="NativePointer.OwnsOwnData"/>.
57+
/// If true, this object owns the underlying pointer and is responsible for
58+
/// calling its destructor when disposed.
59+
/// </summary>
4360
internal bool _ownsPtr;
61+
62+
/// <summary>
63+
/// Indicates whether the object has been disposed.
64+
/// Used to implement the <see cref="System.IDisposable"/> pattern in subclasses.
65+
/// </summary>
4466
internal bool _disposed = false;
4567

68+
/// <summary>
69+
/// Gets the name of the name-mangled native type pointed to by <see cref="_ptrValue"/>.
70+
/// </summary>
4671
internal virtual string _safeTypeName { get; }
4772

48-
public bool PointerIsValid => _ptr != IntPtr.Zero;
73+
/// <summary>
74+
/// Is the underlying pointer (<see cref="_ptr"/>) valid.
75+
/// </summary>
76+
/// <remarks>
77+
/// This can be used to prevent a <see cref="NullReferenceException"/> when calling <see cref="_ptr"/>.
78+
/// </remarks>
79+
public bool PointerIsValid => _ptrValue != IntPtr.Zero;
80+
81+
/// <summary>
82+
/// Pointer to the native object.
83+
/// </summary>
84+
/// <exception cref="NullReferenceException">Thrown if the pointer is null.</exception>
85+
/// <exception cref="InvalidOperationException">Thrown if attempting to change the pointer once it's set to a non-null value.</exception>
86+
internal IntPtr _ptr
87+
{
88+
get
89+
{
90+
// Prevent accessing the pointer if it's null
91+
if (_ptrValue == IntPtr.Zero)
92+
{
93+
throw new NullReferenceException($"Attempting to access a null pointer for {_safeTypeName}");
94+
}
95+
return _ptrValue;
96+
}
97+
set
98+
{
99+
// Prevent changing the pointer once it's set to a non-null value
100+
if (_ptrValue != IntPtr.Zero && value != IntPtr.Zero)
101+
{
102+
throw new InvalidOperationException($"Attempting to change the native pointer for {_safeTypeName} from {_ptrValue} to {value}");
103+
}
104+
105+
_ptrValue = value;
106+
}
107+
}
49108

109+
/// <summary>
110+
/// Construct an empty instance of the object.
111+
/// </summary>
112+
/// <remarks>
113+
/// This is generally an invalid operation since it creates an object where
114+
/// <see cref="_ptr"/> is guaranteed to throw an exception.
115+
/// This empty constructor is currently required by generated generic types
116+
/// (such as <see cref="Csp.Common.List<T>"/>) which look up the underlying
117+
/// base type stored in <see cref="_safeTypeName"/>.
118+
/// </remarks>
50119
public NativeClassWrapper() { }
51120

121+
/// <summary>
122+
/// Construct an instance of the object given a <see cref="NativePointer"/>
123+
/// from the native runtime.
124+
/// </summary>
125+
/// <param name="ptr">A valid <see cref="NativePointer"/> representing the runtime object.</param>
52126
internal NativeClassWrapper(NativePointer ptr)
53127
{
54128
_ptr = ptr.Pointer;

Tools/WrapperGenerator/Templates/CSharp/Partials/Class/Method/Destructor.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ void RealDispose() {
99
if (_ownsPtr && !_disposed)
1010
{
1111
{{ unique_name }}(_ptr);
12-
_disposed = true;
1312
}
1413

14+
_ptr = IntPtr.Zero;
1515
_disposed = true;
1616
}
1717

Tools/WrapperGenerator/Templates/CSharp/Partials/Template/Method/Destructor.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ void RealDispose() {
88
if (_ownsPtr && !_disposed)
99
{
1010
{{ unique_name }}(_ptr);
11-
_disposed = true;
1211
}
1312

13+
_ptr = IntPtr.Zero;
1414
_disposed = true;
1515
}
1616

UnitTesting/include/classes.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ class CSP_API SimpleClass
2424
public:
2525
SimpleClass();
2626
~SimpleClass();
27+
28+
int GetValue() const;
2729
};
2830
}

UnitTesting/src/classes.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ namespace csp::Tests
2121
SimpleClass::SimpleClass() { }
2222

2323
SimpleClass::~SimpleClass() { }
24-
}
24+
25+
int SimpleClass::GetValue() const { return 42; }
26+
27+
} // namespace csp::Tests

UnitTesting/tests/csharp/Assets/Tests/ClassTests.cs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
using NUnit.Framework;
18+
using System;
1819

1920
namespace Csp.Tests
2021
{
@@ -29,13 +30,13 @@ public void CreateAndDestroySimpleClass()
2930

3031
// Verify the internal pointer is set.
3132
// Note that we have to use reflection here to access the internal field.
32-
var ptrField = typeof(SimpleClass).GetField("_ptr", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
33-
var ptrValue = ptrField.GetValue(simpleClass);
33+
var ptrField = typeof(NativeClassWrapper).GetField("_ptrValue", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
34+
IntPtr ptrValue = (IntPtr)ptrField.GetValue(simpleClass);
3435
Assert.IsNotNull(ptrValue);
3536

3637
// Verify that the instance owns the native pointer.
3738
// Note that we have to use reflection here to access the internal field.
38-
var ownsPtrField = typeof(SimpleClass).GetField("_ownsPtr", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
39+
var ownsPtrField = typeof(NativeClassWrapper).GetField("_ownsPtr", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
3940
var ownsPtrValue = ownsPtrField.GetValue(simpleClass);
4041
Assert.IsTrue((bool)ownsPtrValue);
4142

@@ -44,14 +45,19 @@ public void CreateAndDestroySimpleClass()
4445
simpleClass.Dispose();
4546

4647
// Once Dispose is called, the internal pointer is no longer valid.
47-
// However, Dispose does not null out the pointer so the class is
48-
// left in an indeterminate state.
49-
// There is also no way to query whether Dispose has been called.
50-
Assert.IsTrue(simpleClass.PointerIsValid);
48+
Assert.IsFalse(simpleClass.PointerIsValid);
5149

52-
// Verify the internal pointer is still set.
53-
var ptrValueDisposed = ptrField.GetValue(simpleClass);
54-
Assert.IsNotNull(ptrValueDisposed);
50+
// Verify the internal pointer has been nulled.
51+
IntPtr ptrValueDisposed = (IntPtr)ptrField.GetValue(simpleClass);
52+
Assert.AreEqual(IntPtr.Zero, ptrValueDisposed);
53+
}
54+
55+
[Test]
56+
public void GetValueReturnsCorrectValue()
57+
{
58+
var simpleClass = new SimpleClass();
59+
int value = simpleClass.GetValue();
60+
Assert.AreEqual(42, value);
5561
}
5662

5763
[Test]
@@ -63,5 +69,17 @@ public void MultipleDisposes()
6369
simpleClass.Dispose();
6470
simpleClass.Dispose();
6571
}
72+
73+
[Test]
74+
public void AccessAfterDisposeThrows()
75+
{
76+
var simpleClass = new SimpleClass();
77+
simpleClass.Dispose();
78+
79+
// Accessing the pointer after Dispose should throw a NullReferenceException rather
80+
// than creating a segfault.
81+
var ex = Assert.Throws<NullReferenceException>(() => { int value = simpleClass.GetValue(); });
82+
StringAssert.Contains("Attempting to access a null pointer", ex.Message);
83+
}
6684
}
6785
}

0 commit comments

Comments
 (0)