-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Throw from CodeBase if assembly is in the bundle #40974
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
Conversation
|
I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label. |
|
@swaroop-sridhar It looks like I've somehow screwed up the QCall, but I'm not sure how. Advice? |
|
@agocke, it requires a line after runtime/src/coreclr/src/vm/ecalllist.h Line 442 in e8c0a4c
|
|
Is there a way for users to determine that an assembly has been loaded from memory without trying to check codebase and handling this exception? |
|
@Wraith2 If you only want to check whether an Assembly was loaded from memory, calling This API is specifically because I need to know if the file is embedded in a single-file bundle, not that it's just in memory. |
|
To test this we'll need a test for single-file, since this case will only be hit in single-file apps now. |
| get | ||
| { | ||
| var runtimeAssembly = this; | ||
| if (IsInBundle(new QCallAssembly(ref runtimeAssembly))) |
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.
Do we really need a new QCall for this?
We could either throw from native (I know, that's tedious) or we could return null from the GetCodeBase QCall. As far as I can tell the native implementation of GetCodeBase will never return null.
I just don't want to introduce a new QCall if we don't need one (And also so far the managed parts of runtime were not aware of bundles).
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 agree that it is cleaner to do the check within GetCodeBase() in this case.
We may eventually require the IsBundle() QCall API, but can sidestep it for now.
src/coreclr/src/vm/pefile.cpp
Outdated
| result.Set(GetEffectivePath()); | ||
| if (GetILimage()->IsInBundle()) | ||
| { | ||
| result.Set(SString::Empty()); |
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.
Basically here we would return null. Currently it looks weird that we make the same check twice in a row.
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 got lost here -- how would we return null? It looks like SString turns all null pointers into empty strings. Do I need to change the QCall signature?
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.
The QCall returns string, so that should not be a problem. AssemblyNative::GetCodeBase (Which is the QCall) should basically call retString.Set(NULL) - we will likely need to move the bundle logic into this method (or somewhat change the PEAssembly::GetCodeBase to be able to return null).
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 think the problem I'm hitting is
runtime/src/coreclr/src/utilcode/sstring.cpp
Lines 299 to 300 in 6072e4d
| if (string == NULL || *string == 0) | |
| Clear(); |
result.Set(NULL) ends up using an empty string internally.
Any suggestions?
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.
Do you actually need to handle the bundle case here? Can the entire fix be to just throw exception in AssemblyNative::GetCodeBase?
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 code path is also called by GetAssemblyName, and I think that should still work. I think it's fine if we set the CodeBase to null or empty string in the resulting AssemblyName, but I think GetAssemblyName is still a perfectly reasonable call.
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.
Ah, I see. I think what you have here is fine. I do not see a problem with IsInBundle being checked twice. Patterns like that happen all the time.
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.
Alternatively I could change the GetCodeBase call to return something other than void and use that as a sentinel for whether or not the app is in a bundle vs simply loaded from memory.
Personally I kind of like this approach, but if the pattern is frowned upon in the runtime code I'll keep the second QCall
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.
If it's too complicated I don't have a problem with the current implementation - it works. I was just trying to see if there's a simpler way...
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 could change the GetCodeBase call to return something other than
voidand use that as a sentinel for whether or not the app is in a bundle vs simply loaded from memory.Personally I kind of like this approach
I like it too, because it identifies the intention (ex: differentiates from retuning null/empty-string due to an unrelated bug).
| var inMemBlob = File.ReadAllBytes(SourceTestAssemblyPath); | ||
| var asm = Assembly.Load(inMemBlob); | ||
| #pragma warning disable SYSLIB0012 | ||
| Assert.Throws<PlatformNotSupportedException>(() => asm.CodeBase); |
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.
For non-single-file this should not throw - right? I don't understand this test...
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.
Test is wrong now. I originally was throwing from everyhting that was in-memory before I figured out we actually doc'd the behavior. I need to write a new test using single-file, but don't know where to write the test
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.
Based on this comment I think this is a placeholder, and the single-file test is TBD.
But I think CodeBase_InMemory should check Assert.NotEmpty(asm.CodeBase); and the single-file test should be added to AppHost.Bundle 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.
I need to write a new test using single-file, but don't know where to write the test
@agocke I think the easiest way to add a single-file test for this is to follow the BundleProbe test in AppHost.Bundle.Tests.
|
Tagging subscribers to this area: @swaroop-sridhar, @agocke |
| <value>Value was either too large or too small for a UInt64.</value> | ||
| </data> | ||
| <data name="PlatformNotSupported_CodeBase" xml:space="preserve"> | ||
| <value>CodeBase is not supported on assemblies loaded from memory.</value> |
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 think this is a bit confusing. For assemblies loaded from byte-arrays, CodeBase doesn't throw, but returns the caller's location. So, it is better to say assemblies loaded from single-file bundle.
| if (IsInBundle(new QCallAssembly(ref runtimeAssembly))) | ||
| { | ||
| // Not supported if the assembly was loaded from memory | ||
| throw new PlatformNotSupportedException(SR.PlatformNotSupported_CodeBase); |
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 think PlatformNotSupportedException is not the best exception to throw here. I understand that .net native used this exception, and we may keep using it for that reason. Unlike .net native, it is difficult to conceptualize .net apps and .net single-file apps to be separate platforms. Probably a better choice would be InvalidOperationException.
Also, should we tag this PR as a breaking change, because Assembly.CodeBase couldn't throw before, and now it does.
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 agree that PlatformNotSupportedException is not the right exception to throw here. I think it may be more appropriate to throw NotSupportedException instead of InvalidOperationException.
I think InvalidOperationException is appropriate for cases where the operation was valid on this instance but it is not valid anymore; or when the state of the instance can change for the operation to become valid.
It is not the case here. The Assembly instance will always throw, no state will affect it.
| } | ||
|
|
||
| retString.Set(codebase); | ||
| return ret; |
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.
return ret; should be after END_QCALL;
|
Hmm I seem to have broken a number of calls to the GetCodeBase method, but I can't figure out how. All the scenarios show the stack: Any suggestions on debugging this? |
You can download .dmp linked from the test log and matching artifacts from CI, and open it in windbg. I have done just that and I see that it is a crash at: We are crashing because of |
| public override AssemblyName GetName(bool copiedName) | ||
| { | ||
| string? codeBase = GetCodeBase(copiedName); | ||
| string? codeBase = GetCodeBase(); |
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.
Does this mean that if I have an Assembly asm loaded from the bundle, asm.CodeBase will throw, but asm.GetName().CodeBase will be null?
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.
Yup. I couldn't see a good way of making the behavior match. I don't think there's anything wrong with fetching an AssemblyName, so I don't think that should throw, but since the AssemblyName doesn't have a pointer back to the Assembly there's no way to hook just the CodeBase API, aside from doing something drastic like subclassing AssemblyName just for this scenario.
Open to alternatives though.
e0c34a7 to
76f6ce9
Compare
Also adds test for Module.FullyQualifiedName (cherry picked from commit 0546bd5)
|
@agocke I assume we should port this change to the Mono version of |
|
@akoeplinger I was under the impression that Mono didn't support single-file publishing. Was that wrong? Mono might want to change the behavior for AOT though. |
|
@agocke it doesn't now but it could in the future. I'm asking because we have kind of a similar situation with WASM/Blazor where the assembly doesn't exist on "disk" so I think it'd make sense to throw in that case too. |
No description provided.