Skip to content

Commit

Permalink
Implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
antonioaversa committed Feb 13, 2023
1 parent 5c33344 commit 322e626
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 21 deletions.
31 changes: 18 additions & 13 deletions analyzers/rspec/cs/S2445_c#.html
@@ -1,30 +1,35 @@
<p>Synchronizing on a class field synchronizes not on the field itself, but on the object assigned to it. So synchronizing on a non-<code>final</code>
field makes it possible for the field’s value to change while a thread is in a block synchronized on the old value. That would allow a second thread,
synchronized on the new value, to enter the block at the same time.</p>
<p>The story is very similar for synchronizing on parameters; two different threads running the method in parallel could pass two different object
instances in to the method as parameters, completely undermining the synchronization.</p>
<p>Locking on a class field synchronizes not on the field itself, but on the object assigned to it. So locking on a non-<code>readonly</code> field
makes it possible for the field’s value to change while a thread is in a block locked on the old value. That would allow a second thread, locked on
the new value, to enter the block at the same time.</p>
<p>The story is very similar for locking on a local variable or on a new instance; two different threads running the method in parallel would lock on
two different object instances, completely undermining the synchronization.</p>
<h2>Noncompliant Code Example</h2>
<pre>
private String color = "red";
private string color = "red";

private void doSomething(){
synchronized(color) { // Noncompliant; lock is actually on object instance "red" referred to by the color variable
private void DoSomething()
{
lock (color) // Noncompliant; lock is actually on object instance "red" referred to by the color variable
{
//...
color = "green"; // other threads now allowed into this block
// ...
}
synchronized(new Object()) { // Noncompliant this is a no-op.
lock (new object()) // Noncompliant this is a no-op.
{
// ...
}
}
</pre>
<h2>Compliant Solution</h2>
<pre>
private String color = "red";
private final Object lockObj = new Object();
private string color = "red";
private readonly object lockObj = new object();

private void doSomething(){
synchronized(lockObj) {
private void DoSomething()
{
lock (lockObj)
{
//...
color = "green";
// ...
Expand Down
2 changes: 1 addition & 1 deletion analyzers/rspec/cs/S2445_c#.json
@@ -1,5 +1,5 @@
{
"title": "Blocks should be synchronized on \"private final\" fields",
"title": "Blocks should be synchronized on \"private readonly\" fields",
"type": "BUG",
"status": "ready",
"remediation": {
Expand Down
Expand Up @@ -24,20 +24,38 @@ namespace SonarAnalyzer.Rules.CSharp;
public sealed class SynchronizedFieldAssignment : SonarDiagnosticAnalyzer
{
private const string DiagnosticId = "S2445";
private const string MessageFormat = "FIXME";

private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat);
private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, "{0}");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(c =>
{
var node = c.Node;
if (true)
var expression = ((LockStatementSyntax)c.Node).Expression;
if (expression is ObjectCreationExpressionSyntax
or AnonymousObjectCreationExpressionSyntax
or ArrayCreationExpressionSyntax
or ImplicitArrayCreationExpressionSyntax
or QueryExpressionSyntax)
{
c.ReportIssue(Diagnostic.Create(Rule, node.GetLocation()));
c.ReportIssue(Diagnostic.Create(Rule, expression.GetLocation(), "Locking on a new instance is a no-op."));
}
else if (expression.IsAnyKind(SyntaxKind.StringLiteralExpression, SyntaxKind.InterpolatedStringExpression))
{
c.ReportIssue(Diagnostic.Create(Rule, expression.GetLocation(), "Strings can be interned, and should not be used for locking."));
}
else if (expression is IdentifierNameSyntax
&& c.SemanticModel.GetSymbolInfo(expression).Symbol is ILocalSymbol lockedSymbol)
{
c.ReportIssue(Diagnostic.Create(Rule, expression.GetLocation(), $"'{lockedSymbol.Name}' is a local variable, and should not be used for locking."));
}
else if (expression is (IdentifierNameSyntax or MemberAccessExpressionSyntax)
&& c.SemanticModel.GetSymbolInfo(expression).Symbol is IFieldSymbol lockedField
&& !lockedField.IsReadOnly)
{
c.ReportIssue(Diagnostic.Create(Rule, expression.GetLocation(), $"'{lockedField.Name}' is not 'private readonly', and should not be used for locking."));
}
},
SyntaxKind.InvocationExpression);
SyntaxKind.LockStatement);
}
Expand Up @@ -30,4 +30,18 @@ public class SynchronizedFieldAssignmentTest
[TestMethod]
public void SynchronizedFieldAssignment_CS() =>
builder.AddPaths("SynchronizedFieldAssignment.cs").Verify();

[TestMethod]
public void SynchronizedFieldAssignment_CSharp8() =>
builder
.AddPaths("SynchronizedFieldAssignment.CSharp8.cs")
.WithOptions(ParseOptionsHelper.FromCSharp8)
.Verify();

[TestMethod]
public void SynchronizedFieldAssignment_CSharp11() =>
builder
.AddPaths("SynchronizedFieldAssignment.CSharp11.cs")
.WithOptions(ParseOptionsHelper.FromCSharp11)
.Verify();
}
@@ -0,0 +1,21 @@
using System;

class Test
{
static readonly object staticReadonlyField = null;
static object staticReadWriteField = null;

readonly object readonlyField = null;
object readWriteField = null;

void OnANewInstance()
{
lock ("""a raw string literal""") { } // Noncompliant
lock ($"""an interpolated {"raw string literal"}""") { } // Noncompliant
}

void TargetTypedObjectCreation()
{
lock (new() as Tuple<int>) { } // Error [CS8754]
}
}
@@ -0,0 +1,43 @@
using System;

class Test
{
static readonly object staticReadonlyField = null;
static object staticReadWriteField = null;

readonly object readonlyField = null;
object readWriteField = null;

Test()
{
ref object refToReadonlyField = ref readonlyField;
lock (refToReadonlyField) { } // Noncompliant, while the reference is to a readonly field, the reference itself is a local variable and as of C# 7.3 can be ref reassigned

ref object refToReadWriteField = ref readWriteField;
lock (refToReadWriteField) { } // Noncompliant
}

void ReadonlyReferences()
{
lock (RefReturnReadonlyField(this)) { }
lock (RefReturnStaticReadonlyField()) { }
lock (StaticRefReturnReadonlyField(this)) { }
lock (StaticRefReturnStaticReadonlyField()) { }

ref readonly object RefReturnReadonlyField(Test instance) => ref instance.readonlyField;
ref readonly object RefReturnStaticReadonlyField() => ref Test.staticReadonlyField;
static ref readonly object StaticRefReturnReadonlyField(Test instance) => ref instance.readonlyField;
static ref readonly object StaticRefReturnStaticReadonlyField() => ref Test.staticReadonlyField;
}

void OnANewInstanceOnStack()
{
lock (stackalloc int[] { }) { } // Error [CS0185]
lock (stackalloc [] { 1 }) { } // Error [CS0185]
}

void CoalescingAssignment(object oPar)
{
lock (oPar ??= readonlyField) { } // FN, null conditional assignment not supported
}
}
@@ -1,5 +1,151 @@
using System;
using System.Security.Cryptography.X509Certificates;
using System.Linq;

public class Program
class Test
{
static readonly object staticReadonlyField = null;
static object staticReadWriteField = null;

readonly object readonlyField = null;
readonly string readonlyStringField = "a string";
object readWriteField = null;

static object StaticReadonlyProperty => null;
object ReadonlyProperty => null;

static object StaticReadWriteProperty { get; set; }
object ReadWriteProperty { get; set; }

void OnAStaticField()
{
lock (staticReadonlyField) { }
lock (staticReadWriteField) { } // Noncompliant {{'staticReadWriteField' is not 'readonly', and should not be used for synchronization.}}
// ^^^^^^^^^^^^^^^^^^^^
lock (Test.staticReadonlyField) { }
lock (Test.staticReadWriteField) { } // Noncompliant {{'staticReadWriteField' is not 'readonly', and should not be used for synchronization.}}
// ^^^^^^^^^^^^^^^^^^^^^^^^^
lock (AnotherClass.staticReadonlyField) { }
lock (AnotherClass.staticReadWriteField) { } // Noncompliant {{'staticReadWriteField' is not 'readonly', and should not be used for synchronization.}}
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}

void OnAFieldOfSameInstance()
{
lock (readonlyField) { }
lock (readonlyStringField) { }
lock (readWriteField) { } // Noncompliant {{'readWriteField' is not 'readonly', and should not be used for synchronization.}}
lock (this.readonlyField) { }
lock (this.readWriteField) { } // Noncompliant {{'readWriteField' is not 'readonly', and should not be used for synchronization.}}
}

void OnAFieldOfDifferentInstance()
{
var anotherInstance = new Test();
lock (anotherInstance.readonlyField) { }
lock (anotherInstance.readWriteField) { } // Noncompliant {{'readWriteField' is not 'readonly', and should not be used for synchronization.}}
lock (anotherInstance.readonlyField) { }
}

void OnALocalVariable()
{
object localVarNull = null;
lock (localVarNull) { } // Noncompliant {{'localVarNull' is a local variable, and should not be used for synchronization.}}
object localVarReadonlyField = readonlyField;
lock (localVarReadonlyField) { } // Noncompliant, while the local variable references a readonly field, the local variable itself can mutate
object localVarReadWriteField = readWriteField;
lock (localVarReadWriteField) { } // Noncompliant
}

void OnANewInstance()
{
lock (new object()) { } // Noncompliant {{Synchronizing on a new instance is a no-op.}}
lock (new ANamespace.AClass()) { } // Noncompliant
lock (new Test[] { }) // Noncompliant
lock (new[] { readonlyField}) { } // Noncompliant
lock (new Tuple<object>(readonlyField)) { } // Noncompliant
lock (new { }) // Noncompliant

lock ("a string") { } // Noncompliant
lock ($"an interpolated {"string"}") { } // Noncompliant
lock (1) { } // Error [CS0185]
lock ((a: readonlyField, b: readonlyField)) { } // Error [CS0185]

lock (new ADelegate(x => x)) { } // Noncompliant
lock (new Func<int, int>(x => x)) { } // Noncompliant
lock (x => x) { } // Error [CS0185]
lock ((int?)1) { } // Error [CS0185]

lock (from x in new object[2] select x) { } // Noncompliant
}

void OnAssignment()
{
object x;
lock (x = readonlyField) { }
lock (x = readWriteField) { } // FN, assignment not supported
}

void OtherCases(object oPar, bool bPar)
{
lock (null) { }

lock (oPar) { }

lock (this) { }

lock (SomeMethod()) { }
lock (oPar.GetType()) { }
lock (typeof(Test)) { }
lock (default(Test)) { }

object SomeMethod() => null;

lock (StaticReadonlyProperty) { }
lock (ReadonlyProperty) { }
lock (StaticReadWriteProperty) { }
lock (ReadWriteProperty) { }

lock (bPar ? readWriteField : readonlyField) { }

lock (oPar ?? readonlyField) { }
lock (oPar = readonlyField) { }
}

void ReadWriteReferences()
{
lock (RefReturnReadWriteField(this)) { } // FN, the method returns a readwrite ref to a member
lock (RefReturnStaticReadonlyField(this)) { } // FN, the method returns a readwrite ref to a member

ref object RefReturnReadWriteField(Test instance) => ref instance.readWriteField;
ref object RefReturnStaticReadonlyField(Test instance) => ref Test.staticReadWriteField;
}

delegate object ADelegate(object oPar);
}

class TestExplicitCast
{
readonly object readonlyField = null;

void Test()
{
lock ((AnotherClass)readonlyField) { } // Compliant, the cast operator can build
}
}

class AnotherClass
{
public static readonly object staticReadonlyField = null;
public static object staticReadWriteField = null;

public readonly object readonlyField = null;
public object readWriteField = null;

public static explicit operator AnotherClass(Test o) => new AnotherClass();
}

namespace ANamespace
{
class AClass { }
}

0 comments on commit 322e626

Please sign in to comment.