-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Fix muxer handling of symlinks on Windows #87717
Conversation
Tagging subscribers to this area: @vitek-karas, @agocke, @VSadov |
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.
My main concern if perf. In 6.0 we intentionally avoided file existence checks for lot of the files, specifically anything from .deps.json
. Could you please confirm that this doesn't effectively bring that penalty back? If we call realpath
on the assets from .deps.json
, that would effectively add back the file existence check. (I don't remember if we call realpath
in that case).
.CaptureStdOut() | ||
.CaptureStdErr() | ||
.EnvironmentVariable("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "1") | ||
.EnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP", "0"); // Avoid looking at machine state by default |
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.
Nit:
.EnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP", "0"); // Avoid looking at machine state by default | |
.MultilevelLookup(false); // Avoid looking at machine state by default |
I'd also really like for @elinor-fung to review this as well. |
This isn't ready yet, just trying to see what breaks if I change the helper. My basic principles are:
More broadly, I see a couple possible uses for realpath, including constructing a canonical path string for internal use and constructing a path that will be used for relative file path resolution. For the first one, I don't think it's important to resolve symlinks. If we use realpath for that, we can use any other canonicalization form and it doesn't matter. For the latter, I think it's important to do relative directory resolution from the path of the destination, not the path of the symlink. Right now I was trying to just change the realpath behavior to the Unix behavior and see what broke on Windows. More broadly, I think it would be reasonable to audit the uses of realpath in the code to try to separate canonicalization from relative directory analysis. The former would not use realpath, while the latter would. |
Perf would be my concern as well. I believe we skipped the file existence checks for .deps parsing by default by avoiding |
realpath is guaranteed to resolve symlinks. fullpath may resolve symlinks. Moves all existing code to call fullpath, which is the existing contract. On Unix, fullpath calls realpath, meaning it resolves symlinks. On Windows it doesn't. Code which requires symlinks to be resolved should be moved to call realpath. realpath is temporarily renamed realpath2 in this commit to find dangling references.
I think this is ready for review. I've added a new The new semantics are: I've walked through each call and added a comment specifying exactly which I think is appropriate and why. |
Also, I think that none of the realpath calls should have any perf concerns (only hitting a small number of files), but I'd like a double check on that. |
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 want to make sure we treat this with the right risk awareness. This change modifies the behavior of hostfxr.dll
and dotnet.exe
(on Windows only, Linux remains the same).
These are global "effectively unversioned" components (we always use latest version). So for example just installing the latest 8 Preview with this change will change the behavior for all .NET apps/invocation on the machine, even those targeting .NET 7 and other official releases.
I do agree that the risk is relatively low as usage of symlinks on Windows is limitted (especially since it requires switching the machine into a dev mode), but still.
I must admit this makes me nervous since we don't have a "backward compat" behavior at all - if this breaks somebody there's no workaround.
I think we should spend some time on figuring out the real potential impact of this change - and if it seems non-trivial, then probably run it through official triage as a real breaking change. The bar on this should be really high due to the global nature of hostfxr
/dotnet
/
@@ -139,7 +140,7 @@ int exe_start(const int argc, const pal::char_t* argv[]) | |||
{ | |||
trace::info(_X("Detected Single-File app bundle")); | |||
} | |||
else if (!pal::realpath(&app_path)) | |||
else if (!pal::realpath(&app_path)) // Use realpath to find the app path through symlinks |
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.
To properly test this we would need to setup two symlinks. The executable can be a symlink, we should look for the app.dll
next to the resolved path (after resolving the symlink in host_path
) and then the app.dll
could also be a symlink, which we will resolve here.
It's an interesting question if we should actually behave like this. We will use the app_path
for everything else (.deps.json
, .runtimeconfig.json
, app base path, ...).
I guess we want to keep working this way since we've done it forever on Linux - and so making Windows match Linux in this case makes a lot of sense (since previous to this change Windows symlink support was messy).
@@ -545,6 +545,7 @@ namespace | |||
|
|||
if (startup_info.host_path.empty()) | |||
{ | |||
// Use realpath to find the host_path behind symlinks |
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.
Should we also "realpath" the host_path
if it is specified by the caller? This comes from custom host code (so external input).
I think for consistency it would make sense to follow symlinks everywhere, but it is technically a breaking change - and in hostfxr :-(
bool pal::realpath(string_t* path, bool skip_error_logging) | ||
typedef std::unique_ptr<std::remove_pointer<HANDLE>::type, decltype(&::CloseHandle)> SmartHandle; | ||
|
||
// Like realpath, but resolves symlinks. |
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.
Should it be?
// Like realpath, but resolves symlinks. | |
// Like fullpath, but resolves symlinks. |
// Remove the \\?\ prefix, unless it is necessary or was already there | ||
if (LongFile::IsExtended(str) && !LongFile::IsExtended(*path) && | ||
!LongFile::ShouldNormalize(str.substr(LongFile::ExtendedPrefix.size()))) | ||
{ | ||
str.erase(0, LongFile::ExtendedPrefix.size()); | ||
} |
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.
Just an observation:
According to the docs the first check (LongFile::IsExtended(str)
) will pretty much always return true, since the GetFinalPathNameByHandleW
is supposed to return the \\?\
prefixed path. The second check that the original path is not extended is also going to be true almost always. So we will end up calling ShouldNormalize
and very likely erase
below as well.
Can we try to make this somehow cheaper? (or maybe it's not worth it) - this will likely allocate twice (substr
and erase
) which makes this method allocate at least 3 times (the last is the str.assign
above).
Maybe I'm just too careful and it's not worth the complexity to optimize this.
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.
Might be worth adding a comment that the Win32 API call will typically return \\?\
prefixed path.
// We use realpath here because we want the final path through symlinks for the app_root. | ||
if (pal::fullpath(&args.managed_application)) |
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 comment and code disagree here.
I think we want realpath in code as well.
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.
Apparently we're missing a test for this case.
@@ -20,6 +20,31 @@ public PortableAppActivation(SharedTestState fixture) | |||
sharedTestState = fixture; | |||
} | |||
|
|||
[Fact] |
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.
Ideally we would add tests for all use cases of realpath
in the code.
app_candidate = host_info.app_path; | ||
// Use realpath since we want the path to the app through symlinks | ||
doesAppExist = bundle::info_t::is_single_file_bundle() || pal::realpath(&app_candidate); |
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 not in this PR, but it would be worth looking into this a bit higher level. For example in this case, the host_info.app_path
might already be after symlink resolution (in fact it typically will be, since if you run apphost.exe
we will populate this as resolved via the code in corehost which also uses realpath).
But there are other places which initialize the host_info.app_path
to paths which are unresolved yet.
Would be nice to have a principle that it either is guaranteed resolved, or not (in which case the callers don't try to resolve at all).
bool pal::fullpath(pal::string_t* path, bool skip_error_logging) | ||
{ | ||
return realpath(path, skip_error_logging); | ||
} | ||
|
||
bool pal::realpath(pal::string_t* path, bool skip_error_logging) |
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 deserves a comment - especially since the implementation is "the other way round" from Windows.
bool realpath(string_t* path, bool skip_error_logging = false); | ||
bool fullpath(string_t* path, bool skip_error_logging = false); |
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.
Please add a comment what is the difference between the two - the naming is Unix based, but Unix doesn't have fullpath
, so it's unclear what that does.
Agreed on testing and impact -- I'll add more testing to validate these changes. |
@vitek-karas Do you think it's worth scoping this change down to just the muxer, because of the risk? |
That would help only somewhat. I guess we could try to avoid doing the change in In general I'm in favor of this change. If nothing else, it brings Windows and Linux closer in behavior. I just want us to be careful, that's all. Maybe we should keep this for now and merge it first thing into .NET 9, so that we have an entire year of previews to validate (and also .NET 8 is LTS...). The global nature of muxer/hostfxr means that if somebody wants these changes, installing .NET 9 Preview 1 will be enough to "fix it" for everything. |
Sounds good to me. |
@agocke, this hasn't been touched in a year. Should it be closed? Are you still working on it? |
Closing out, I don't think we need to take this until we get customer feedback on other parts that don't work behind symlinks. |
Fixes #83314