-
Notifications
You must be signed in to change notification settings - Fork 470
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
Prevent interceptors from being able to modify in
parameters
#370
Conversation
afdaf81
to
0e55d36
Compare
Add a test fixture that verifies whether interceptors can cause mutations only via mutable by-ref parameters. In particular, `in` parameters should protect against mutations.
57fe307
to
730cb19
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work, thanks! 👍
Left a few minor comments to make the code even better 😉
// | ||
// The comparison by name is intentional; any assembly could define that attribute. | ||
// See explanation in comment below. | ||
Func<object, bool> isIsReadOnlyAttribute = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Create a local method for this - it will allow compiler to cache the value better (only once, rather than per each invocation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've run a few experiments to see the compiler automatically cache instantiated delegates, but I haven't been able to observe such behavior (neither in a debug nor release builds)...?
The compiler will quite literally do what it's told, so the important thing is to take the hidden delegate instantiation out of the loop... it's a quick and easy win. Writing the lambda in-place instead of in a local function or static method produces less line noise. IMO we can suffer that one delegate instantiation per invocation—if not, we'd possibly have to refactor a lot of other code in DynamicProxy!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Playing with it more I found this issue: dotnet/roslyn#19431. That's the reason whey are not currently cached. If you pass anonymous non-capturing lambda and target e.g. net462
you'll see the caching in action:
foreach (var parameter in parameters)
{
if (parameter.attributes.Any(t => t.FullName == "EEEE"))
{
// Do smth
}
}
Compiled:
foreach (System.ValueTuple<string, Type[]> parameter in parameters)
{
if (!parameter.Item2.Any(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = <>c.<>9.<CycleWithLocalMethod>b__0_0)))
continue;
}
Later they might enable the similar optimization for the local functions - that's why I suggested it.
Also it looks more natural with local methods. But as this a matter of tastes - it's up to you 😉
// | ||
// The above points inform the following detection logic: First, we rely on an IL | ||
// `[in]` modifier being present. This is a "fast guard" for non-`in` parameters: | ||
if ((parameters[i].Attributes & ParameterAttributes.In) != 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As to me, this method started to have too many responsibilities. Better create a standalone method like ArgumentsUtil.IsReadOnlyByRefArg()
or similar and put all the detection logic there.
Also it might make sense to create ArgumentsUtil.IsAnyWritableByRef
or similar and use it instead of ArgumentsUtil.IsAnyByRef
, so we don't generate e.g. redundant try/catch
when we are not going to write something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that the method is called CopyOutAndRefParameters
, I think the additional 6 lines of code are still in scope.
That said, I do agree that it could be re-written to make better use of small helper methods to raise the level of abstraction.
As you already noted, AttributeUtil
might be a good place to have additional helpers similar to IsAnyByRef
. But once we start adding methods there, we also have to think about accessibility for consistency's sake. AttributeUtil.IsAnyByRef
is public
which I think was a mistake. So either we expand Core's public API further by adding new public methods, or we add non-public methods and are therefore inconsistent (albeit for good, practical reasons), or we add method(s) in a different place, which can also be seen as inconsistent.
This is a design problem I'd rather not solve today. May I leave it up to you to submit a PR that cleans this method up a little?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, if you don't want to produce too much noise, then I'd suggest to simply extract all the IsReadOnlyRef
logic to a static method in the same class. Each method should have single responsibility only, while currently CopyOutAndRefParameters
method orchestrates parameters handling and knows the particularity of how in
modifier is implemented.
|
||
[Test] | ||
[TestCase(typeof(IByReadOnlyRef))] | ||
public void By_reference_In_parameter_has_ParameterAttributes_In_set(Type type) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are mixing facade (or integration) tests here and our expectations about compiler. I'd remove all the tests about expectations, as:
- it's unclear what to do when they start to fail;
- facade tests will start to fail anyway, so these are redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
facade tests will start to fail anyway, so these are redundant.
Their added value lies in giving us an additional, very direct hint at why the unit tests might be failing. I've done that kind of testing in a few other places recently.
@jonorossi has previously been in favor of these kinds of tests, so I'd like to leave it up to him whether to remove these tests or not.
it's unclear what to do when they start to fail
Isn't that true for most tests?
It would be magical if unit tests instructed us precisely how to fix them once they break (for whatever reason). I'd be interested to learn how to write such tests, can you point me to any resources?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm definitely in favour of keeping these additional tests that are sort of like proofs, if it happens the implementation of the runtime changes or we misunderstood how it works we'll know straight away. They have heaps of value for DynamicProxy where they wouldn't in most libraries because we are so low in the stack and make so many assumptions about implementation details that a lot of the time aren't documented in the ECMA specifications.
The only thing that would be good is a "heading-like" comment like you've done in the others to separate them from the actual unit tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'll add such a comment! Done.
9cfc82a
to
6f10335
Compare
@zvirja, thanks for your review. You know what, I think you were right. I've rewritten the method to make use of local functions, and I've marked |
{ | ||
var parameter = type.GetMethod("Method").GetParameters()[0]; | ||
|
||
Assert.True(parameter.GetCustomAttributes().Any(a => a.GetType().FullName == "System.Runtime.CompilerServices.IsReadOnlyAttribute")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought these classes were public classes, I assume they aren't visible in .NET Standard 1.3? Read below, wow! 🥇
void Method(out ReadOnlyStruct arg); | ||
} | ||
|
||
public sealed class SetArgumentValueInterceptor : IInterceptor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class should probably sit alongside Castle.DynamicProxy.Tests.Interceptors.SetReturnValueInterceptor
, and also accept an index.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
CHANGELOG.md
Outdated
- Prevent interceptors from being able to modify `in` parameters (@stakx, #370) | ||
|
||
Deprecations: | ||
- `ArgumentsUtil.IsAnyByRef` (@stakx, #370) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Way too much of the DP API is marked public including this class. Probably worthwhile writing Castle.DynamicProxy.Generators.Emitters.ArgumentsUtil.IsAnyByRef
otherwise someone might be confused with one of the classes people might actually use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
||
return false; | ||
|
||
bool IsIsReadOnlyAttribute(object attribute) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it is just me but 3 levels of nested methods makes CopyOutAndRefParameters
appear quite complicated when in actual fact it isn't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jonorossi: 😆, I almost thought that might be your reaction!
The idea here is to make the scope of helper functions as local as possible, so that it's clear where they are actually needed. Contrast this with a "global" helper function like ArgumentsUtil.IsAnyByRef
which, as it turned out, was only used in one single location—but you couldn't tell by taking a casual glance at its source.
Options:
- Leave it as it is now (nested local functions).
- Bring
IsIsReadOnlyAttribute
to the same level as the two other local functions to reduce nesting. - Turn the three local functions into
private static
methods in the same class (GeneratorUtil
). - Revert to how things were before (i.e. don't have helper functions/methods at all).
What's your preference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just leave it as is, I didn't really have a preference other than being a little surprised. I think if CopyOutAndRefParameters
was non-void it would look strange because you'd have to try to match up returns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor comments, looks great.
6f10335
to
aa5515f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stakx All good. I'll let you do the honours of merging it, please squish it either manually or via the GitHub UI.
@jonorossi - thanks for entrusting me with the reigns at this point. ❤️ Unfortunately, Core's |
Fixed. Castle Core and Windsor have "protected" master branches from when there were other people working on a single .NET Core branch that weren't committers. |
Hi @stakx, I'm trying to consume this change in NSubstitute and have complications with the way we perform the check.
The complication is that for .NET Framework the compiler generates the You'll see the same issue with Moq if you decide to guarantee that mock for delegate with I see a way to fix that (generate Core/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs Lines 77 to 86 in 1ba6c89
Why can't we simply check for the
modifier is always available for the virtual and abstract methods which we are dealing with only. (BTW, it looks like a typo in spec, as they use I believe we shouldn't worry about .NET Standard 1.3 as Thanks. Sorry for not testing this before the release - I was almost confident that everything must work fine... 😖 P.S. Just tested the check like this: #if FEATURE_EMIT_CUSTOMMODIFIERS
if (parameter.GetRequiredCustomModifiers().Any(mod => mod == typeof(InAttribute)))
{
return true;
}
#endif Works like a charm for all .NET Framework and .NET Core versions. |
Thank you for following up on this. I should have realised in time that the compiler-generated
Core/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs Lines 70 to 73 in 1ba6c89
Like you're saying, we could do this. However, I've spent quite a bit of time with cmods in the past few months, and it wasn't super enjoyable. None of the major runtimes (Mono, .NET Framework, .NET Core) support cmods very well when it comes to Reflection and Reflection.Emit. I think it entirely possible that yet more cmod-related bugs in these facilities will come to the fore as C# continues to rely on them more and more. Hence I figured it would be best to avoid them if possible, and decided against relying on cmods for the detection logic. Add to this the fact that the primary characteristic of C# I guess we could have a detection logic that tests for either cmods and custom attributes (or both). I considered this, but decided against it as a trade-off because checking two things takes more time than one thing. Given that we have a fast-guard check for But for the time being at least, you'll have to emit a
(Agree! I've taken the liberty to ask over at dotnet/csharplang whether this is a typo or not. Let's see what they say.) |
Could you please be more specific here? What exact issues did you see? I'm just curios, as I tested a few versions of both .NET Framework and .NET Core and it worked well there. Probably, not the generics (as you already reported), but apart from that it worked fine.
Looks like a great trade-off! If method has It will add an additional robustness to the detection logic and support non-perfect code-gen scenarios. And would also simplify the NSubstitute code as it will be enough to emit custom modifier only 😊 I could raise a PR for that if you wish.
Thanks for registering that issue 👍 Likely I should be that proactive to do it by myself... 😖 |
Fair question. I should be more specific, but in truth, I cannot. I guess it comes down to being a trust issue for me. Consider: Custom modifiers have historically been used very little outside C++/CLI, whose community was relatively small (compared to e.g. that of C#). Add to this that the typical use case for C++/CLI was to be a fast and easy interop bridge between managed and unmanaged/native code, not to do heavy-duty reflection stuff like DynamicProxy does. So if there are in fact cmod-related bugs in .NET's managed Reflection API, that would likely have gone unnoticed by most people for the longest time. Not even Microsoft's compiler team might have noticed since the compilers were native programs using the CLR's native APIs. C# has started to use cmods for its own purposes only quite recently, and almost right away we walk into bugs/limitations with Reflection. (You can look up at some of the work I did in DynamicProxy, or some of the issues I raised over at the Mono, CoreCLR, and CoreFX repos. The CoreCLR issues are also representative for the CLR.) That's not exactly reassuring. Who knows what else we might discover. On the other hand, custom attributes have long been used by the C# community, so they are much more "battle-tested". This is why I trust in them a lot more than I trust in Reflection's ability to handle cmods properly. It doesn't help that one of the code owners of Reflection / Reflection.Emit admits (here) that Reflection has fundamental limitations (even though they might not matter much in practice) when it comes to cmods:
That all being said, you're of course right that the very scenario we're looking at doesn't appear to be affected. I'll let you judge for yourself whether it's right to be cautious about cmods or not.
Have you checked on Mono, as well?
(I guess you meant "if the parameter has the Yes, precisely.
Sounds great! I feel a little saturated with cmod-related work right now, so I'd be totally happy to defer to your initiative! One suggestion, though: It would be good to make sure that the faster of the two checks is made first. I'm suspecting that the
|
@stakx Thanks for the verbose reply! 😉
I see your point now. Likely you are right that cmods support is quite limited and indeed your possibilities are quite limited. However, probably, for our case it doesn't matter. In any case if we tests for both modifiers and attributes, than I suppose we play safely 😉
Sure. Will do after this one is closed.
Sure :) In the meanwhile I've adjusted the NSubstitute to generate the |
This should fix #369.