Skip to content

Commit c1a08ff

Browse files
committed
Specify lifetime of ByRefLikeArguments
1 parent 615ba2d commit c1a08ff

File tree

2 files changed

+56
-0
lines changed

2 files changed

+56
-0
lines changed

src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,43 @@ public void By_ref_like_out_arguments_cannot_be_set_by_target()
500500

501501
#endregion
502502

503+
#region Memory safety
504+
505+
// Byref-like arguments live exclusively on the evaluation stack.
506+
// We need to make sure that references to them (such as those in `IInvocation.Arguments`)
507+
// do not have a longer lifetime than the arguments themselves.
508+
509+
[Test]
510+
public unsafe void Cannot_use_ByRefLikeArgument_GetPointer_after_invocation()
511+
{
512+
var interceptor = new ObservingInterceptor();
513+
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
514+
proxy.Method(default);
515+
var byRefLikeArg = (ByRefLikeArgument)interceptor.ObservedArg!;
516+
Assert.Throws<ObjectDisposedException>(() => _ = byRefLikeArg.GetPointer());
517+
}
518+
519+
[Test]
520+
public void Cannot_use_ByRefLikeArgument_Get_after_invocation()
521+
{
522+
var interceptor = new ObservingInterceptor();
523+
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
524+
proxy.Method(default);
525+
var byRefLikeArg = (ReadOnlySpanArgument<char>)interceptor.ObservedArg!;
526+
Assert.Throws<ObjectDisposedException>(() => _ = byRefLikeArg.Get());
527+
}
528+
529+
[Test]
530+
public void ByRefLikeArguments_are_erased_from_invocation_Arguments_after_invocation()
531+
{
532+
var interceptor = new ObservingInterceptor();
533+
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
534+
proxy.Method(default);
535+
Assert.IsNull(interceptor.AllArguments![0]);
536+
}
537+
538+
#endregion
539+
503540
public class HasMethodWithSpanParameter
504541
{
505542
public string? RecordedArg;
@@ -541,10 +578,12 @@ public virtual void Method(out ReadOnlySpan<char> arg)
541578

542579
public class ObservingInterceptor : IInterceptor
543580
{
581+
public object?[]? AllArguments;
544582
public object? ObservedArg;
545583

546584
public void Intercept(IInvocation invocation)
547585
{
586+
AllArguments = invocation.Arguments;
548587
ObservedArg = invocation.Arguments[0];
549588
}
550589
}

src/Castle.Core/DynamicProxy/ByRefLikeArgument.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ public ByRefLikeArgument(void* ptr)
7878
/// <summary>
7979
/// Gets an unmanaged pointer to the byref-like (<c>ref struct</c>) argument.
8080
/// </summary>
81+
/// <remarks>
82+
/// <para>
83+
/// You may only use the returned pointer during the first run through the interception pipeline.
84+
/// After that, it will be invalid, because the argument that it referred to will be gone
85+
/// from the evaluation stack.
86+
/// </para>
87+
/// <para>
88+
/// In particular, if you intercept an <see langword="async"/> method and make use of
89+
/// <see cref="IInvocationProceedInfo"/> to proceed through the pipeline again
90+
/// after an <see langword="await"/>, you may no longer access any byref-like arguments.
91+
/// (.NET compilers would forbid any such attempts, too.)
92+
/// </para>
93+
/// <para>
94+
/// Using the returned pointer beyond the lifetime of the byref-like argument
95+
/// will cause undefined behavior, or an <see cref="AccessViolationException"/> at best.
96+
/// </para>
97+
/// </remarks>
8198
[CLSCompliant(false)]
8299
[EditorBrowsable(EditorBrowsableState.Advanced)]
83100
public void* GetPointer()

0 commit comments

Comments
 (0)