-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Generated IL for &local
in an async
method in debug contains a GC hole
#63100
Comments
is there any expectation that an unfixed pointer ever be safe to read? I don't get hwo the code above would be safe even if there was no async. The location the pointer points to has nothing ensuring it stays alive (as far as the c# language is concerned). -- Specifically, we say:
As such, any address not computed by a fixed-pointer-initializer does not have such a guarantee, and it could be subject to relocation. |
Locals are implicitly fixed. That's why C# allows you to take address of them without |
Gotcha. Thanks! If the async-section doesn't state anything about these locals not really being locals, then i def agree with this assessment. I'm guessing that if it did though, then we'd also have an error about allowing |
If the variable is lifted to a closure the compiler reports an error (example below [*]). Seems like the same should happen for variables lifted to state machine. In DEBUG we lift all local variables to state machine, in RELEASE we might not if the variable life span doesn't cross await expression/yield statement. I guess we have two options:
static async Task LogValueAsync()
{
Span<int> s = stackalloc int[1];
await Task.CompletedTask;
}
[*] static void LogValue()
{
int x = 50;
var f = new Func<int>(() => x);
unsafe
{
int* p = &x;
Console.Write('\r');
Console.WriteLine(*p);
}
}
|
Interesting: we disallow any unsafe code in iterators.
but stack-only types are allowed as long as their life span does not cross yield return: OK: static IEnumerable<int> Iter()
{
Span<int> s = stackalloc int[1];
Console.Write(s[0]);
yield return 1;
yield return 2;
} not ok: static IEnumerable<int> Iter()
{
Span<int> s = stackalloc int[1];
yield return 1;
Console.Write(s[0]);
yield return 2;
}
|
Notably, you can still take the address of a local in an using System;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
await LogValueAsync();
static async Task LogValueAsync()
{
int x = 50;
await Task.Delay(1);
unsafe
{
_ = Unsafe.AsPointer(ref x); // or Unsafe.AsRef(in x);
int* p = &x;
Console.Write('\r');
Console.WriteLine(*p);
}
await Task.CompletedTask;
}
The sample (for release builds) can be simplified further to: using System;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
int x = 50;
await Task.Delay(1);
Unsafe.AsRef(in x);
unsafe
{
int* p = &x;
Console.Write('\r');
Console.WriteLine(*p);
} |
@DaZombieKiller Right, I think the compiler should implement the current behavior of iterators for async and lambdas as well and report errors in these cases. It should also allow stack-only types to be used for locals whose lifespan does not cross await/yield return and not lift them in debug builds. |
From discussion with @MadsTorgersen, another option is to rewrite the address-of into a |
@jcouv Any opinion on #63100 (comment)? |
@jcouv Specifically, re
|
@tmat Yes, we discussed an option like that, for locals that don't need lifting and don't span across suspensions. The main drawback is that it is a deeper change to the async rewriter, as opposed to (hopefully) a localized change. |
FYI: #66829 may be in the same area here when working on a fix. |
We might want a warning when a local or parameter's address is taken without 'fixed' in an async method. Since the hoisting can depend on whether you build in debug or release mode, etc., it's maybe better to just have a consistent rule for it. |
The analogous behavior for lambda captures is to just disallow taking the address. Maybe it should be a warning or error (breaking change) to take the address of any local or parameter in an async method, since it may be hoisted. Not sure. |
In #66915 my solution is tentatively to issue a warning in .NET 8 when The idea here is to just give you a nudge that: hey, things may go wrong here, maybe delegate this work to another method and call it instead. This is an alternative to us coming up with a more complex solution in the compiler, such as implicitly fixing variables that compile into class fields, or permitting+requiring fixed-statements with them, etc. |
#47423 is tangentially related here. |
Closed by #66915 |
Steps to Reproduce:
DOTNET_GCStress=2
Expected Behavior:
The program logs
50
to the console.Actual Behavior:
The program logs a garbage value to the console.
The
x
local is emitted as a field in aclass
under debug, and Roslyn emits IL that takes its address.Workarounds:
The GC hole can be avoided by using
Unsafe.AsRef
to allow the use offixed
:The text was updated successfully, but these errors were encountered: