Skip to content

Commit 67db9de

Browse files
committed
Introduce ByRefLikeReferenceUnsafe and use it in generated code
By it's nature Unsafe methods are not a part of the "client" API, so we just provide it on a side and should no longer hide methods/constructors from user. Also unsafe API is internal on the original class, so it's not immediately visible to the users.
1 parent bf2b1fa commit 67db9de

10 files changed

Lines changed: 195 additions & 137 deletions

File tree

ref/Castle.Core-net8.0.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,13 +2424,20 @@ public virtual bool ShouldInterceptMethod(System.Type type, System.Reflection.Me
24242424
}
24252425
public class ByRefLikeReference
24262426
{
2427-
[System.CLSCompliant(false)]
2428-
public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { }
24292427
public bool ValueIsScoped { get; }
2428+
}
2429+
public static class ByRefLikeReferenceUnsafe
2430+
{
2431+
[System.CLSCompliant(false)]
2432+
public static unsafe Castle.DynamicProxy.ByRefLikeReference CreateByRefLikeReferenceUntyped(System.Type type, void* ptr, bool valueIsScoped) { }
24302433
[System.CLSCompliant(false)]
2431-
public unsafe void* GetPtr(System.Type checkType) { }
2434+
public static unsafe Castle.DynamicProxy.ReadOnlySpanReference<T> CreateReadOnlySpanReference<T>(System.Type type, void* ptr, bool valueIsScoped) { }
24322435
[System.CLSCompliant(false)]
2433-
public unsafe void Invalidate(void* checkPtr) { }
2436+
public static unsafe Castle.DynamicProxy.SpanReference<T> CreateSpanReference<T>(System.Type type, void* ptr, bool valueIsScoped) { }
2437+
[System.CLSCompliant(false)]
2438+
public static unsafe void DisposeReference(Castle.DynamicProxy.ByRefLikeReference reference, void* expectedPtr) { }
2439+
[System.CLSCompliant(false)]
2440+
public static unsafe void* GetRawPtr(Castle.DynamicProxy.ByRefLikeReference reference, System.Type expectedType) { }
24342441
}
24352442
public class CustomAttributeInfo : System.IEquatable<Castle.DynamicProxy.CustomAttributeInfo>
24362443
{
@@ -2702,8 +2709,6 @@ public static bool IsProxyType(System.Type type) { }
27022709
}
27032710
public class ReadOnlySpanReference<T> : Castle.DynamicProxy.ByRefLikeReference
27042711
{
2705-
[System.CLSCompliant(false)]
2706-
public ReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { }
27072712
public System.ReadOnlySpan<T> GetValue() { }
27082713
public void SetValue(Castle.DynamicProxy.ReadOnlySpanReference<T>.ValueGetter valueGetter) { }
27092714
public void UseValue(Castle.DynamicProxy.ReadOnlySpanReference<T>.ValueConsumer valueConsumer) { }
@@ -2714,8 +2719,6 @@ public TResult UseValue<TResult>(Castle.DynamicProxy.ReadOnlySpanReference<T>.Va
27142719
}
27152720
public class SpanReference<T> : Castle.DynamicProxy.ByRefLikeReference
27162721
{
2717-
[System.CLSCompliant(false)]
2718-
public SpanReference(System.Type type, void* ptr, bool valueIsScoped) { }
27192722
public System.Span<T> GetValue() { }
27202723
public void SetValue(Castle.DynamicProxy.SpanReference<T>.ValueGetter valueGetter) { }
27212724
public void UseValue(Castle.DynamicProxy.SpanReference<T>.ValueConsumer valueConsumer) { }

ref/Castle.Core-net9.0.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,19 +2424,27 @@ public virtual bool ShouldInterceptMethod(System.Type type, System.Reflection.Me
24242424
}
24252425
public class ByRefLikeReference
24262426
{
2427-
[System.CLSCompliant(false)]
2428-
public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { }
24292427
public bool ValueIsScoped { get; }
2428+
}
2429+
public static class ByRefLikeReferenceUnsafe
2430+
{
2431+
[System.CLSCompliant(false)]
2432+
public static unsafe Castle.DynamicProxy.ByRefLikeReference<TByRefLike> CreateByRefLikeReference<TByRefLike>(System.Type type, void* ptr, bool valueIsScoped)
2433+
where TByRefLike : struct { }
2434+
[System.CLSCompliant(false)]
2435+
public static unsafe Castle.DynamicProxy.ByRefLikeReference CreateByRefLikeReferenceUntyped(System.Type type, void* ptr, bool valueIsScoped) { }
2436+
[System.CLSCompliant(false)]
2437+
public static unsafe Castle.DynamicProxy.ReadOnlySpanReference<T> CreateReadOnlySpanReference<T>(System.Type type, void* ptr, bool valueIsScoped) { }
2438+
[System.CLSCompliant(false)]
2439+
public static unsafe Castle.DynamicProxy.SpanReference<T> CreateSpanReference<T>(System.Type type, void* ptr, bool valueIsScoped) { }
24302440
[System.CLSCompliant(false)]
2431-
public unsafe void* GetPtr(System.Type checkType) { }
2441+
public static unsafe void DisposeReference(Castle.DynamicProxy.ByRefLikeReference reference, void* expectedPtr) { }
24322442
[System.CLSCompliant(false)]
2433-
public unsafe void Invalidate(void* checkPtr) { }
2443+
public static unsafe void* GetRawPtr(Castle.DynamicProxy.ByRefLikeReference reference, System.Type expectedType) { }
24342444
}
24352445
public class ByRefLikeReference<TByRefLike> : Castle.DynamicProxy.ByRefLikeReference
24362446
where TByRefLike : struct
24372447
{
2438-
[System.CLSCompliant(false)]
2439-
public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { }
24402448
public TByRefLike GetValue() { }
24412449
public void SetValue(Castle.DynamicProxy.ByRefLikeReference<TByRefLike>.ValueGetter valueGetter) { }
24422450
public void UseValue(Castle.DynamicProxy.ByRefLikeReference<TByRefLike>.ValueConsumer valueConsumer) { }
@@ -2723,16 +2731,8 @@ public static bool IsAccessible(System.Reflection.MethodBase method, [System.Dia
27232731
public static bool IsProxy(object? instance) { }
27242732
public static bool IsProxyType(System.Type type) { }
27252733
}
2726-
public class ReadOnlySpanReference<T> : Castle.DynamicProxy.ByRefLikeReference<System.ReadOnlySpan<T>>
2727-
{
2728-
[System.CLSCompliant(false)]
2729-
public ReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { }
2730-
}
2731-
public class SpanReference<T> : Castle.DynamicProxy.ByRefLikeReference<System.Span<T>>
2732-
{
2733-
[System.CLSCompliant(false)]
2734-
public SpanReference(System.Type type, void* ptr, bool valueIsScoped) { }
2735-
}
2734+
public class ReadOnlySpanReference<T> : Castle.DynamicProxy.ByRefLikeReference<System.ReadOnlySpan<T>> { }
2735+
public class SpanReference<T> : Castle.DynamicProxy.ByRefLikeReference<System.Span<T>> { }
27362736
public class StandardInterceptor : Castle.DynamicProxy.IInterceptor
27372737
{
27382738
public StandardInterceptor() { }

src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,32 +63,32 @@ public unsafe void Ctor_preserves_value_is_scoped_value(bool valueIsScoped)
6363
}
6464

6565
[Test]
66-
public unsafe void Invalidate_throws_if_address_mismatch()
66+
public unsafe void Dispose_throws_if_address_mismatch()
6767
{
6868
ReadOnlySpan<char> local = default;
6969
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local, false);
7070
Assert.Throws<InvalidOperationException>(() =>
7171
{
7272
ReadOnlySpan<char> otherLocal = default;
73-
reference.Invalidate(&otherLocal);
73+
reference.Dispose(&otherLocal);
7474
});
7575
}
7676

7777
[Test]
78-
public unsafe void Invalidate_succeeds_if_address_match()
78+
public unsafe void Dispose_succeeds_if_address_match()
7979
{
8080
ReadOnlySpan<char> local = default;
8181
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local, false);
82-
reference.Invalidate(&local);
82+
reference.Dispose(&local);
8383
}
8484

8585
[Test]
86-
public unsafe void Invalidate_throws_when_access_from_other_thread()
86+
public unsafe void Dispose_throws_when_access_from_other_thread()
8787
{
8888
ReadOnlySpan<char> local = default;
8989
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local, false);
9090
var address = reference.GetPtr(typeof(ReadOnlySpan<char>));
91-
var task = Task.Run(() => reference.Invalidate(address));
91+
var task = Task.Run(() => reference.Dispose(address));
9292
var msg = Assert.Throws<InvalidOperationException>(() => task.GetAwaiter().GetResult()).Message;
9393
StringAssert.Contains("thread", msg);
9494
}
@@ -111,11 +111,11 @@ public unsafe void GetPtr_returns_ctor_address_if_type_match()
111111
}
112112

113113
[Test]
114-
public unsafe void GetPtr_throws_after_Invalidate()
114+
public unsafe void GetPtr_throws_after_Dispose()
115115
{
116116
ReadOnlySpan<char> local = default;
117117
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local, false);
118-
reference.Invalidate(&local);
118+
reference.Dispose(&local);
119119
Assert.Throws<ObjectDisposedException>(() => reference.GetPtr(typeof(ReadOnlySpan<char>)));
120120
}
121121

src/Castle.Core/DynamicProxy/ByRefLikeReference.cs

Lines changed: 20 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
namespace Castle.DynamicProxy
2121
{
2222
using System;
23-
using System.ComponentModel;
2423
using System.Diagnostics;
2524
using System.Diagnostics.CodeAnalysis;
2625
using System.Threading;
@@ -42,45 +41,43 @@ namespace Castle.DynamicProxy
4241
//
4342
// *) Unmanaged pointers can be safe when used to reference stack-allocated objects. However, that is only true
4443
// when they point into "live" stack frames. That is, they MUST NOT reference parameters or local variables
45-
// of methods that have already finished executing. This is why we have the `ByRefLikeReference.Invalidate` method:
44+
// of methods that have already finished executing. This is why we have the `ByRefLikeReference.Dispose` method:
4645
// DynamicProxy (or whatever else instantiated a `ByRefLikeReference` object to point at a method parameter or local
4746
// variable) must invoke this method before said method returns (or tail-calls).
4847
//
49-
// *) The `checkType` / `checkPtr` arguments of `GetPtr` or `Invalidate`, respectively, have two purposes:
48+
// *) The `checkType` / `checkPtr` arguments of `GetPtr` or `Dispose`, respectively, have two purposes:
5049
//
5150
// 1. DynamicProxy, or whatever else instantiated a `ByRefLikeReference`, is expected to know at all times what
5251
// exactly each instance references. These parameters make it harder for anyone to use the type directly
5352
// if they didn't also instantiate it themselves.
5453
//
55-
// 2. `checkPtr` of `Invalidate` attempts to prevent re-use of a referenced storage location for another
54+
// 2. `checkPtr` of `Dispose` attempts to prevent re-use of a referenced storage location for another
5655
// similarly-typed local variable by the JIT. DynamicProxy typically instantiates `ByRefLikeReference` instances
57-
// at the start of intercepted method bodies, and it invokes `Invalidate` at the very end, meaning that
56+
// at the start of intercepted method bodies, and it invokes `Dispose` at the very end, meaning that
5857
// the address of the local/parameter is taken at each method boundary, meaning that static analysis should
5958
// never during the whole method see the local/parameter as "no longer in use". (This may be a little
6059
// paranoid, since the CoreCLR JIT probably exempts so-called "address-exposed" locals from reuse anyway.)
6160
//
6261
// *) We track if each reference represents scoped value. Scoped values usually represent data living on stack only,
63-
// so we should apply more strict rules on how we expose the value.
64-
// Dynamic generator analyzes method signature and provides the correct value for us.
62+
// so we should apply more strict rules on how we expose the value. Otherwise, we could have use-after-free scenario,
63+
// which could lead to stack corruption and weird behavior.
64+
// Dynamic generator analyzes method signature and provides the correct value.
6565
//
6666
// *) We always return a copy of the original value and never expose the value by reference.
67-
// This is important to make sure that we prevent scenario of `ref struct` interior mutability
68-
// and providing a potential way to leak scoped values out of their lifetime scope.
67+
// This is important to make sure that we prevent possibility `ref struct` interior mutability (as copy will be changed)
68+
// and close a possibility to leak scoped values out of their lifetime scope by storing them inside another `ref struct`.
6969
//
7070
// *) The SetValue() function uses delegate to get the value. This is required to make sure that local variables
71-
// or variables with scoped visibility are not promoted outside their lifetime.
71+
// or variables with scoped visibility could never be promoted outside their lifetime.
7272
// When we have delegate, we could only use heap-backed values and compiler will enforce the safety for us.
7373
//
7474
// *) Finally, we allow accessing reference data from the owning thread only to avoid all possible concurrency-related issues.
75+
// Otherwise, it's very easy to have use-after-free scenario, as other thread could access value after function exit.
7576
//
7677
// As far as I can reason, `ByRefLikeReference` et al. should be safe to use IFF they are never copied out from an
7778
// `IInvocation`, and IFF DynamicProxy succeeds in destructing them and erasing them from the `IInvocation` right
7879
// before the intercepted method finishes executing.
7980

80-
/// <summary>
81-
/// Do not use! This type should only be used by DynamicProxy internals.
82-
/// </summary>
83-
[EditorBrowsable(EditorBrowsableState.Never)]
8481
public unsafe class ByRefLikeReference
8582
{
8683
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
@@ -92,13 +89,8 @@ public unsafe class ByRefLikeReference
9289
private Thread ownerThread;
9390

9491
public bool ValueIsScoped { get; }
95-
96-
/// <summary>
97-
/// Do not use! This constructor should only be called by DynamicProxy internals.
98-
/// </summary>
99-
[CLSCompliant(false)]
100-
[EditorBrowsable(EditorBrowsableState.Never)]
101-
public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped)
92+
93+
internal ByRefLikeReference(Type type, void* ptr, bool valueIsScoped)
10294
{
10395
if (type.IsByRefLikeSafe() == false)
10496
{
@@ -116,12 +108,7 @@ public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped)
116108
this.ownerThread = Thread.CurrentThread;
117109
}
118110

119-
/// <summary>
120-
/// Do not use! This method should only be called by DynamicProxy internals.
121-
/// </summary>
122-
[CLSCompliant(false)]
123-
[EditorBrowsable(EditorBrowsableState.Never)]
124-
public void* GetPtr(Type checkType)
111+
internal void* GetPtr(Type checkType)
125112
{
126113
AssertCurrentThread();
127114

@@ -132,24 +119,19 @@ public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped)
132119

133120
if (this.ptr == null)
134121
{
135-
throw new ObjectDisposedException("This reference was already invalidated");
122+
throw new ObjectDisposedException("This reference was already disposed");
136123
}
137124

138125
return this.ptr;
139126
}
140127

141-
/// <summary>
142-
/// Do not use! This method should only be called by DynamicProxy internals.
143-
/// </summary>
144-
[CLSCompliant(false)]
145-
[EditorBrowsable(EditorBrowsableState.Never)]
146-
public void Invalidate(void* checkPtr)
128+
internal void Dispose(void* checkPtr)
147129
{
148130
AssertCurrentThread();
149131

150132
if (this.ptr == null || this.ptr != checkPtr)
151133
{
152-
throw new InvalidOperationException($"BUG: Pointer mismatch on reference invalidation. Expected: {(nint)checkPtr:X16}, Actual: {(nint)this.ptr:X16}");
134+
throw new InvalidOperationException($"BUG: Pointer mismatch on reference disposal. Expected: {(nint)checkPtr:X16}, Actual: {(nint)this.ptr:X16}");
153135
}
154136

155137
this.ptr = null;
@@ -183,12 +165,7 @@ private void AssertCurrentThread()
183165
public unsafe class ByRefLikeReference<TByRefLike> : ByRefLikeReference
184166
where TByRefLike : struct, allows ref struct
185167
{
186-
/// <summary>
187-
/// Do not use! This constructor should only be called by DynamicProxy internals.
188-
/// </summary>
189-
[CLSCompliant(false)]
190-
[EditorBrowsable(EditorBrowsableState.Never)]
191-
public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped)
168+
internal ByRefLikeReference(Type type, void* ptr, bool valueIsScoped)
192169
: base(type, ptr, valueIsScoped)
193170
{
194171
if (type != typeof(TByRefLike))
@@ -259,12 +236,7 @@ public unsafe class ReadOnlySpanReference<T>
259236
: ByRefLikeReference
260237
#endif
261238
{
262-
/// <summary>
263-
/// Do not use! This constructor should only be called by DynamicProxy internals.
264-
/// </summary>
265-
[CLSCompliant(false)]
266-
[EditorBrowsable(EditorBrowsableState.Never)]
267-
public ReadOnlySpanReference(Type type, void* ptr, bool valueIsScoped)
239+
internal ReadOnlySpanReference(Type type, void* ptr, bool valueIsScoped)
268240
: base(type, ptr, valueIsScoped)
269241
{
270242
if (type != typeof(ReadOnlySpan<T>))
@@ -336,12 +308,7 @@ public unsafe class SpanReference<T>
336308
: ByRefLikeReference
337309
#endif
338310
{
339-
/// <summary>
340-
/// Do not use! This constructor should only be called by DynamicProxy internals.
341-
/// </summary>
342-
[CLSCompliant(false)]
343-
[EditorBrowsable(EditorBrowsableState.Never)]
344-
public SpanReference(Type type, void* ptr, bool valueIsScoped)
311+
internal SpanReference(Type type, void* ptr, bool valueIsScoped)
345312
: base(type, ptr, valueIsScoped)
346313
{
347314
if (type != typeof(Span<T>))
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#if FEATURE_BYREFLIKE
16+
#nullable enable
17+
18+
namespace Castle.DynamicProxy;
19+
20+
using System;
21+
22+
/// <summary>
23+
/// Collection of methods to perform unsafe manipulations on <see cref="ByRefLikeReference"/> instances.
24+
/// Usage could lead to memory safe issues, so be careful when using it
25+
/// </summary>
26+
public static class ByRefLikeReferenceUnsafe
27+
{
28+
[CLSCompliant(false)]
29+
public static unsafe ByRefLikeReference CreateByRefLikeReferenceUntyped(Type type, void* ptr, bool valueIsScoped)
30+
{
31+
return new ByRefLikeReference(type, ptr, valueIsScoped);
32+
}
33+
34+
#if NET9_0_OR_GREATER
35+
36+
[CLSCompliant(false)]
37+
public static unsafe ByRefLikeReference<TByRefLike> CreateByRefLikeReference<TByRefLike>(Type type, void* ptr, bool valueIsScoped)
38+
where TByRefLike : struct, allows ref struct
39+
{
40+
return new ByRefLikeReference<TByRefLike>(type, ptr, valueIsScoped);
41+
}
42+
#endif
43+
44+
[CLSCompliant(false)]
45+
public static unsafe ReadOnlySpanReference<T> CreateReadOnlySpanReference<T>(Type type, void* ptr, bool valueIsScoped)
46+
{
47+
return new ReadOnlySpanReference<T>(type, ptr, valueIsScoped);
48+
}
49+
50+
[CLSCompliant(false)]
51+
public static unsafe SpanReference<T> CreateSpanReference<T>(Type type, void* ptr, bool valueIsScoped)
52+
{
53+
return new SpanReference<T>(type, ptr, valueIsScoped);
54+
}
55+
56+
/// <summary>
57+
/// Returns raw pointer held by the <paramref name="reference"/>.
58+
/// </summary>
59+
[CLSCompliant(false)]
60+
public static unsafe void* GetRawPtr(ByRefLikeReference reference, Type expectedType)
61+
{
62+
return reference.GetPtr(expectedType);
63+
}
64+
65+
/// <summary>
66+
/// Disposes <paramref name="reference"/>, so that underlying value can no longer be used
67+
/// </summary>
68+
[CLSCompliant(false)]
69+
public static unsafe void DisposeReference(ByRefLikeReference reference, void* expectedPtr)
70+
{
71+
reference.Dispose(expectedPtr);
72+
}
73+
}
74+
75+
#endif

src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public void Emit(ILGenerator gen)
5050
{
5151
gen.Emit(OpCodes.Ldtoken, dereferencedArgumentType);
5252
gen.Emit(OpCodes.Call, TypeMethods.GetTypeFromHandle);
53-
gen.Emit(OpCodes.Call, ByRefLikeReferenceMethods.GetPtr);
53+
gen.Emit(OpCodes.Call, ByRefLikeReferenceUnsafeMethods.GetRawPtr);
5454
gen.Emit(OpCodes.Ldobj, dereferencedArgumentType);
5555
}
5656
else

0 commit comments

Comments
 (0)