Skip to content
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 modification of readonly locals through ref fields #74255

Merged
merged 5 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Compilers/CSharp/Portable/BoundTree/Constructors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private static bool NeedsByValueFieldAccess(BoundExpression? receiver, FieldSymb
{
if (fieldSymbol.IsStatic ||
!fieldSymbol.ContainingType.IsValueType ||
fieldSymbol.RefKind is RefKind.Ref ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In L76 we are effectively checking RefKind != RefKind.None on the localSymbol, should we also do that here for the fieldSymbol? Is it possible we need this when we have a ref-readonly field whose referent value is changing "during" the expression?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a ref readonly field, there are binding errors if we try to change its referent value, so I think we cannot observe the difference (the IsByValue computed here is used by Binder.HasHome in codegen to determine whether the field should be stored in a temp but we never get there if we have binding errors). But it makes sense to me to check what you suggest.

receiver == null) // receiver may be null in error cases
{
return false;
Expand Down
120 changes: 120 additions & 0 deletions src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenForEachTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5327,5 +5327,125 @@ public static class Extensions
}";
CompileAndVerify(source, parseOptions: TestOptions.Regular9, expectedOutput: "123123");
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_01()
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a smoke test for LINQ iteration variables and using variables. They have the same "can't assign" semantics as foreach iteration variables and hence are possibly subject to the same issue.

Copy link
Member Author

@jjonescz jjonescz Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are tests for using variables added in this PR in CodeGenUsingStatementTests.cs.
LINQ iteration variables aren't affected by this bug (the iteration variable is not a LocalSymbol so the BoundFieldAccess has always IsByValue set to false), but I will add similar tests.

var source = """
using System;

V[] arr = new V[3];

foreach (var r in new E(arr))
{
r.V.F++;
}

foreach (var v in arr) Console.Write(v.F);

ref struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public R Current => new(ref arr[i - 1]);
public bool MoveNext() => i++ < arr.Length;
}

ref struct R(ref V v)
{
public ref V V = ref v;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The submitted repro has these as readonly ref fields. Let's add some more tests with different variants of readonlyness sprinkled throughout. It would also be good to test variables that come from ref foreach loops.

}

struct V
{
public int F;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.Fails,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "111").VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_02()
{
var source = """
using System;

V[] arr = new V[3];

foreach (var r in new E(arr))
{
r.V.F += 2;
}

foreach (var v in arr) Console.Write(v.F);

ref struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public R Current => new(ref arr[i - 1]);
public bool MoveNext() => i++ < arr.Length;
}

ref struct R(ref V v)
{
public ref V V = ref v;
}

struct V
{
public int F;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.Fails,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "222").VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_03()
{
var source = """
using System;

V[] arr = new V[3];

foreach (var r in new E(arr))
{
r.V.S.Inc();
}

foreach (var v in arr) Console.Write(v.S.F);

ref struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public R Current => new(ref arr[i - 1]);
public bool MoveNext() => i++ < arr.Length;
}

ref struct R(ref V v)
{
public ref V V = ref v;
}

struct V
{
public S S;
}

struct S
{
public int F;
public void Inc() => F++;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.Fails,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "111").VerifyDiagnostics();
}
}
}
100 changes: 100 additions & 0 deletions src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenUsingStatementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#nullable disable

using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;

Expand Down Expand Up @@ -3136,5 +3137,104 @@ ref struct S
}
}
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_01()
{
var source = """
using System;

V v = default;

using (var d = new R(ref v))
{
d.V.F++;
}

Console.Write(v.F);

ref struct R(ref V v)
{
public ref V V = ref v;
public void Dispose() { }
}

struct V
{
public int F;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.FailsPEVerify,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "1").VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_02()
{
var source = """
using System;

V v = default;

using (var d = new R(ref v))
{
d.V.F += 2;
}

Console.Write(v.F);

ref struct R(ref V v)
{
public ref V V = ref v;
public void Dispose() { }
}

struct V
{
public int F;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.FailsPEVerify,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "2").VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_03()
{
var source = """
using System;

V v = default;

using (var d = new R(ref v))
{
d.V.S.Inc();
}

Console.Write(v.S.F);

ref struct R(ref V v)
{
public ref V V = ref v;
public void Dispose() { }
}

struct V
{
public S S;
}

struct S
{
public int F;
public void Inc() => F++;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.FailsPEVerify,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "1").VerifyDiagnostics();
}
}
}
Loading