Skip to content

Commit edeacbb

Browse files
authored
TaskFactoryWrapper: guard against multi-threaded access to dictionaries (#8928)
Fixes https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1824802 This change protects the dictionaries owned by TaskFactoryWrapper from being mutated while being accessed by multiple threads. Change TaskFactoryWrapper's dictionary fields from IDictionary<K,V> to IReadOnlyDictionary<K,V> Add support for IReadOnlyDictionary<K,V> to ReadOnlyEmptyDictionary Ensure mutually exclusive access to TaskFactoryWrapper.PopulatePropertyInfoCacheIfNecessary, which is the place where all of the object's dictionaries are created.
1 parent ce59c70 commit edeacbb

3 files changed

Lines changed: 147 additions & 97 deletions

File tree

src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ bool ITaskExecutionHost.SetTaskParameters(IDictionary<string, (string, ElementLo
333333
// Get the properties that exist on this task. We need to gather all of the ones that are marked
334334
// "required" so that we can keep track of whether or not they all get set.
335335
var setParameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
336-
IDictionary<string, string> requiredParameters = GetNamesOfPropertiesWithRequiredAttribute();
336+
IReadOnlyDictionary<string, string> requiredParameters = GetNamesOfPropertiesWithRequiredAttribute();
337337

338338
// look through all the attributes of the task element
339339
foreach (KeyValuePair<string, (string, ElementLocation)> parameter in parameters)
@@ -1540,10 +1540,10 @@ private void GatherArrayStringAndValueOutputs(bool outputTargetIsItem, string ou
15401540
/// Returns them as keys in a dictionary.
15411541
/// </summary>
15421542
/// <returns>Gets a list of properties which are required.</returns>
1543-
private IDictionary<string, string> GetNamesOfPropertiesWithRequiredAttribute()
1543+
private IReadOnlyDictionary<string, string> GetNamesOfPropertiesWithRequiredAttribute()
15441544
{
15451545
ErrorUtilities.VerifyThrow(_taskFactoryWrapper != null, "Expected taskFactoryWrapper to not be null");
1546-
IDictionary<string, string> requiredParameters = null;
1546+
IReadOnlyDictionary<string, string> requiredParameters = null;
15471547

15481548
try
15491549
{

src/Build/Instance/TaskFactoryWrapper.cs

Lines changed: 108 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
using Microsoft.Build.Framework;
99
using Microsoft.Build.Shared;
1010

11-
#nullable disable
12-
1311
namespace Microsoft.Build.Execution
1412
{
1513
/// <summary>
@@ -19,30 +17,50 @@ internal sealed class TaskFactoryWrapper
1917
{
2018
#region Data
2119

20+
private struct PropertyData
21+
{
22+
/// <summary>
23+
/// Cache of names of required properties on this type
24+
/// </summary>
25+
public readonly IReadOnlyDictionary<string, string> NamesOfPropertiesWithRequiredAttribute;
26+
27+
/// <summary>
28+
/// Cache of names of output properties on this type
29+
/// </summary>
30+
public readonly IReadOnlyDictionary<string, string> NamesOfPropertiesWithOutputAttribute;
31+
32+
/// <summary>
33+
/// Cache of names of properties on this type whose names are ambiguous
34+
/// </summary>
35+
public readonly IReadOnlyDictionary<string, string> NamesOfPropertiesWithAmbiguousMatches;
36+
37+
/// <summary>
38+
/// Cache of PropertyInfos for this type
39+
/// </summary>
40+
public readonly IReadOnlyDictionary<string, TaskPropertyInfo> PropertyInfoCache;
41+
42+
public PropertyData(
43+
IReadOnlyDictionary<string, string> namesOfPropertiesWithRequiredAttribute,
44+
IReadOnlyDictionary<string, string> namesOfPropertiesWithOutputAttribute,
45+
IReadOnlyDictionary<string, string> namesOfPropertiesWithAmbiguousMatches,
46+
IReadOnlyDictionary<string, TaskPropertyInfo> propertyInfoCache)
47+
{
48+
NamesOfPropertiesWithRequiredAttribute = namesOfPropertiesWithRequiredAttribute;
49+
NamesOfPropertiesWithOutputAttribute = namesOfPropertiesWithOutputAttribute;
50+
NamesOfPropertiesWithAmbiguousMatches = namesOfPropertiesWithAmbiguousMatches;
51+
PropertyInfoCache = propertyInfoCache;
52+
}
53+
}
54+
2255
/// <summary>
2356
/// Factory which is wrapped by the wrapper
2457
/// </summary>
2558
private ITaskFactory _taskFactory;
2659

2760
/// <summary>
28-
/// Cache of names of required properties on this type
29-
/// </summary>
30-
private IDictionary<string, string> _namesOfPropertiesWithRequiredAttribute;
31-
32-
/// <summary>
33-
/// Cache of names of output properties on this type
34-
/// </summary>
35-
private IDictionary<string, string> _namesOfPropertiesWithOutputAttribute;
36-
37-
/// <summary>
38-
/// Cache of names of properties on this type whose names are ambiguous
39-
/// </summary>
40-
private IDictionary<string, string> _namesOfPropertiesWithAmbiguousMatches;
41-
42-
/// <summary>
43-
/// Cache of PropertyInfos for this type
61+
/// Wrapper of lazy initializable property data.
4462
/// </summary>
45-
private IDictionary<string, TaskPropertyInfo> _propertyInfoCache;
63+
private Lazy<PropertyData> _propertyData;
4664

4765
/// <summary>
4866
/// The name of the task this factory can create.
@@ -70,6 +88,7 @@ internal TaskFactoryWrapper(ITaskFactory taskFactory, LoadedType taskFactoryLoad
7088
_taskName = taskName;
7189
TaskFactoryLoadedType = taskFactoryLoadInfo;
7290
_factoryIdentityParameters = factoryIdentityParameters;
91+
_propertyData = new Lazy<PropertyData>(PopulatePropertyInfo);
7392
}
7493

7594
#endregion
@@ -101,13 +120,11 @@ public ITaskFactory TaskFactory
101120
/// Caches the result - since it can't change during the build.
102121
/// </summary>
103122
/// <returns></returns>
104-
public IDictionary<string, string> GetNamesOfPropertiesWithRequiredAttribute
123+
public IReadOnlyDictionary<string, string> GetNamesOfPropertiesWithRequiredAttribute
105124
{
106125
get
107126
{
108-
PopulatePropertyInfoCacheIfNecessary();
109-
110-
return _namesOfPropertiesWithRequiredAttribute;
127+
return _propertyData.Value.NamesOfPropertiesWithRequiredAttribute;
111128
}
112129
}
113130

@@ -116,13 +133,11 @@ public IDictionary<string, string> GetNamesOfPropertiesWithRequiredAttribute
116133
/// Caches the result - since it can't change during the build.
117134
/// </summary>
118135
/// <returns></returns>
119-
public IDictionary<string, string> GetNamesOfPropertiesWithOutputAttribute
136+
public IReadOnlyDictionary<string, string> GetNamesOfPropertiesWithOutputAttribute
120137
{
121138
get
122139
{
123-
PopulatePropertyInfoCacheIfNecessary();
124-
125-
return _namesOfPropertiesWithOutputAttribute;
140+
return _propertyData.Value.NamesOfPropertiesWithOutputAttribute;
126141
}
127142
}
128143

@@ -158,18 +173,16 @@ public IDictionary<string, string> FactoryIdentityParameters
158173
/// </summary>
159174
/// <param name="propertyName">property name</param>
160175
/// <returns>PropertyInfo</returns>
161-
public TaskPropertyInfo GetProperty(string propertyName)
176+
public TaskPropertyInfo? GetProperty(string propertyName)
162177
{
163-
PopulatePropertyInfoCacheIfNecessary();
164-
165-
TaskPropertyInfo propertyInfo;
166-
if (!_propertyInfoCache.TryGetValue(propertyName, out propertyInfo))
178+
TaskPropertyInfo? propertyInfo;
179+
if (!_propertyData.Value.PropertyInfoCache.TryGetValue(propertyName, out propertyInfo))
167180
{
168181
return null;
169182
}
170183
else
171184
{
172-
if (_namesOfPropertiesWithAmbiguousMatches.ContainsKey(propertyName))
185+
if (_propertyData.Value.NamesOfPropertiesWithAmbiguousMatches.ContainsKey(propertyName))
173186
{
174187
// See comment in PopulatePropertyInfoCache
175188
throw new AmbiguousMatchException();
@@ -187,37 +200,37 @@ internal void SetPropertyValue(ITask task, TaskPropertyInfo property, object val
187200
ErrorUtilities.VerifyThrowArgumentNull(task, nameof(task));
188201
ErrorUtilities.VerifyThrowArgumentNull(property, nameof(property));
189202

190-
IGeneratedTask generatedTask = task as IGeneratedTask;
203+
IGeneratedTask? generatedTask = task as IGeneratedTask;
191204
if (generatedTask != null)
192205
{
193206
generatedTask.SetPropertyValue(property, value);
194207
}
195208
else
196209
{
197210
ReflectableTaskPropertyInfo propertyInfo = (ReflectableTaskPropertyInfo)property;
198-
propertyInfo.Reflection.SetValue(task, value, null);
211+
propertyInfo.Reflection?.SetValue(task, value, null);
199212
}
200213
}
201214

202215
/// <summary>
203216
/// Gets the value of a given property on the given task.
204217
/// </summary>
205-
internal object GetPropertyValue(ITask task, TaskPropertyInfo property)
218+
internal object? GetPropertyValue(ITask task, TaskPropertyInfo property)
206219
{
207220
ErrorUtilities.VerifyThrowArgumentNull(task, nameof(task));
208221
ErrorUtilities.VerifyThrowArgumentNull(property, nameof(property));
209222

210-
IGeneratedTask generatedTask = task as IGeneratedTask;
223+
IGeneratedTask? generatedTask = task as IGeneratedTask;
211224
if (generatedTask != null)
212225
{
213226
return generatedTask.GetPropertyValue(property);
214227
}
215228
else
216229
{
217-
ReflectableTaskPropertyInfo propertyInfo = property as ReflectableTaskPropertyInfo;
230+
ReflectableTaskPropertyInfo? propertyInfo = property as ReflectableTaskPropertyInfo;
218231
if (propertyInfo != null)
219232
{
220-
return propertyInfo.Reflection.GetValue(task, null);
233+
return propertyInfo.Reflection?.GetValue(task, null);
221234
}
222235
else
223236
{
@@ -242,77 +255,79 @@ internal bool IsCreatableByFactory(string taskName)
242255
/// <summary>
243256
/// Populate the cache of PropertyInfos for this type
244257
/// </summary>
245-
private void PopulatePropertyInfoCacheIfNecessary()
258+
private PropertyData PopulatePropertyInfo()
246259
{
247-
if (_propertyInfoCache == null)
260+
Dictionary<string, TaskPropertyInfo>? propertyInfoCache = null;
261+
Dictionary<string, string>? namesOfPropertiesWithRequiredAttribute = null;
262+
Dictionary<string, string>? namesOfPropertiesWithOutputAttribute = null;
263+
Dictionary<string, string>? namesOfPropertiesWithAmbiguousMatches = null;
264+
265+
bool taskTypeImplementsIGeneratedTask = typeof(IGeneratedTask).IsAssignableFrom(_taskFactory.TaskType);
266+
TaskPropertyInfo[] propertyInfos = _taskFactory.GetTaskParameters();
267+
268+
for (int i = 0; i < propertyInfos.Length; i++)
248269
{
249-
bool taskTypeImplementsIGeneratedTask = typeof(IGeneratedTask).IsAssignableFrom(_taskFactory.TaskType);
250-
TaskPropertyInfo[] propertyInfos = _taskFactory.GetTaskParameters();
270+
// If the task implements IGeneratedTask, we must use the TaskPropertyInfo the factory gives us.
271+
// Otherwise, we never have to hand the TaskPropertyInfo back to the task or factory, so we replace
272+
// theirs with one of our own that will allow us to cache reflection data per-property.
273+
TaskPropertyInfo propertyInfo = propertyInfos[i];
274+
if (!taskTypeImplementsIGeneratedTask)
275+
{
276+
propertyInfo = new ReflectableTaskPropertyInfo(propertyInfo, _taskFactory.TaskType);
277+
}
251278

252-
for (int i = 0; i < propertyInfos.Length; i++)
279+
try
253280
{
254-
// If the task implements IGeneratedTask, we must use the TaskPropertyInfo the factory gives us.
255-
// Otherwise, we never have to hand the TaskPropertyInfo back to the task or factory, so we replace
256-
// theirs with one of our own that will allow us to cache reflection data per-property.
257-
TaskPropertyInfo propertyInfo = propertyInfos[i];
258-
if (!taskTypeImplementsIGeneratedTask)
281+
if (propertyInfoCache == null)
259282
{
260-
propertyInfo = new ReflectableTaskPropertyInfo(propertyInfo, _taskFactory.TaskType);
283+
propertyInfoCache = new Dictionary<string, TaskPropertyInfo>(StringComparer.OrdinalIgnoreCase);
261284
}
262285

263-
try
264-
{
265-
if (_propertyInfoCache == null)
266-
{
267-
_propertyInfoCache = new Dictionary<string, TaskPropertyInfo>(StringComparer.OrdinalIgnoreCase);
268-
}
269-
270-
_propertyInfoCache.Add(propertyInfo.Name, propertyInfo);
271-
}
272-
catch (ArgumentException)
286+
propertyInfoCache.Add(propertyInfo.Name, propertyInfo);
287+
}
288+
catch (ArgumentException)
289+
{
290+
// We have encountered a duplicate entry in our hashtable; if we had used BindingFlags.IgnoreCase this
291+
// would have produced an AmbiguousMatchException. In the old code, before this cache existed,
292+
// that wouldn't have been thrown unless and until the project actually tried to set this ambiguous parameter.
293+
// So rather than fail here, we store a list of ambiguous names and throw later, when one of them
294+
// is requested.
295+
if (namesOfPropertiesWithAmbiguousMatches == null)
273296
{
274-
// We have encountered a duplicate entry in our hashtable; if we had used BindingFlags.IgnoreCase this
275-
// would have produced an AmbiguousMatchException. In the old code, before this cache existed,
276-
// that wouldn't have been thrown unless and until the project actually tried to set this ambiguous parameter.
277-
// So rather than fail here, we store a list of ambiguous names and throw later, when one of them
278-
// is requested.
279-
if (_namesOfPropertiesWithAmbiguousMatches == null)
280-
{
281-
_namesOfPropertiesWithAmbiguousMatches = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
282-
}
283-
284-
_namesOfPropertiesWithAmbiguousMatches[propertyInfo.Name] = String.Empty;
297+
namesOfPropertiesWithAmbiguousMatches = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
285298
}
286299

287-
if (propertyInfos[i].Required)
288-
{
289-
if (_namesOfPropertiesWithRequiredAttribute == null)
290-
{
291-
_namesOfPropertiesWithRequiredAttribute = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
292-
}
293-
294-
// we have a require attribute defined, keep a record of that
295-
_namesOfPropertiesWithRequiredAttribute[propertyInfo.Name] = String.Empty;
296-
}
300+
namesOfPropertiesWithAmbiguousMatches[propertyInfo.Name] = String.Empty;
301+
}
297302

298-
if (propertyInfos[i].Output)
303+
if (propertyInfos[i].Required)
304+
{
305+
if (namesOfPropertiesWithRequiredAttribute == null)
299306
{
300-
if (_namesOfPropertiesWithOutputAttribute == null)
301-
{
302-
_namesOfPropertiesWithOutputAttribute = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
303-
}
304-
305-
// we have a output attribute defined, keep a record of that
306-
_namesOfPropertiesWithOutputAttribute[propertyInfo.Name] = String.Empty;
307+
namesOfPropertiesWithRequiredAttribute = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
307308
}
309+
310+
// we have a require attribute defined, keep a record of that
311+
namesOfPropertiesWithRequiredAttribute[propertyInfo.Name] = String.Empty;
308312
}
309313

310-
_propertyInfoCache ??= ReadOnlyEmptyDictionary<string, TaskPropertyInfo>.Instance;
314+
if (propertyInfos[i].Output)
315+
{
316+
if (namesOfPropertiesWithOutputAttribute == null)
317+
{
318+
namesOfPropertiesWithOutputAttribute = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
319+
}
311320

312-
_namesOfPropertiesWithRequiredAttribute ??= ReadOnlyEmptyDictionary<string, string>.Instance;
313-
_namesOfPropertiesWithOutputAttribute ??= ReadOnlyEmptyDictionary<string, string>.Instance;
314-
_namesOfPropertiesWithAmbiguousMatches ??= ReadOnlyEmptyDictionary<string, string>.Instance;
321+
// we have a output attribute defined, keep a record of that
322+
namesOfPropertiesWithOutputAttribute[propertyInfo.Name] = String.Empty;
323+
}
315324
}
325+
326+
return new PropertyData(
327+
(IReadOnlyDictionary<string, string>?)namesOfPropertiesWithRequiredAttribute ?? ReadOnlyEmptyDictionary<string, string>.Instance,
328+
(IReadOnlyDictionary<string, string>?)namesOfPropertiesWithOutputAttribute ?? ReadOnlyEmptyDictionary<string, string>.Instance,
329+
(IReadOnlyDictionary<string, string>?)namesOfPropertiesWithAmbiguousMatches ?? ReadOnlyEmptyDictionary<string, string>.Instance,
330+
(IReadOnlyDictionary<string, TaskPropertyInfo>?)propertyInfoCache ?? ReadOnlyEmptyDictionary<string, TaskPropertyInfo>.Instance);
316331
}
317332
#endregion
318333
}

src/Shared/ReadOnlyEmptyDictionary.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Microsoft.Build.Collections
1616
/// </summary>
1717
/// <typeparam name="K">Key</typeparam>
1818
/// <typeparam name="V">Value</typeparam>
19-
internal class ReadOnlyEmptyDictionary<K, V> : IDictionary<K, V>, IDictionary
19+
internal class ReadOnlyEmptyDictionary<K, V> : IDictionary<K, V>, IReadOnlyDictionary<K, V>, IDictionary
2020
{
2121
/// <summary>
2222
/// The single instance
@@ -127,6 +127,22 @@ ICollection IDictionary.Values
127127
get { return (ICollection)((IDictionary<K, V>)this).Values; }
128128
}
129129

130+
/// <summary>
131+
/// Keys
132+
/// </summary>
133+
IEnumerable<K> IReadOnlyDictionary<K, V>.Keys
134+
{
135+
get { return Keys; }
136+
}
137+
138+
/// <summary>
139+
/// Values
140+
/// </summary>
141+
IEnumerable<V> IReadOnlyDictionary<K, V>.Values
142+
{
143+
get { return Values; }
144+
}
145+
130146
/// <summary>
131147
/// Indexer
132148
/// </summary>
@@ -292,3 +308,22 @@ public void CopyTo(System.Array array, int index)
292308
}
293309
}
294310
}
311+
312+
#if NET35
313+
namespace System.Collections.Generic
314+
{
315+
public interface IReadOnlyCollection<T> : IEnumerable<T>
316+
{
317+
int Count { get; }
318+
}
319+
320+
public interface IReadOnlyDictionary<TKey, TValue> : IReadOnlyCollection<KeyValuePair<TKey, TValue>>
321+
{
322+
TValue this[TKey key] { get; }
323+
IEnumerable<TKey> Keys { get; }
324+
IEnumerable<TValue> Values { get; }
325+
bool ContainsKey(TKey key);
326+
bool TryGetValue(TKey key, out TValue value);
327+
}
328+
}
329+
#endif

0 commit comments

Comments
 (0)