Skip to content

TypeDescriptor.GetProperties(object instance) is not thread-safe #92394

Closed
@seanhalliday

Description

@seanhalliday

Description

If multiple threads call TypeDescriptor.GetProperties(object instance) at the same time, they can get different results if the instance has a TypeDescriptorProvider attribute. I believe this is because of missing locks around reading the WeakHashTables.

Reproduction Steps

using System.ComponentModel;
using System.Threading;

namespace TypeDescriptionProvider_GetProperties_repro
{
	public class SomeTypeProvider : TypeDescriptionProvider
	{
		public static ThreadLocal<bool> Constructed = new ThreadLocal<bool>();
		public static ThreadLocal<bool> GetPropertiesCalled = new ThreadLocal<bool>();
		private class CTD : ICustomTypeDescriptor
		{
			public AttributeCollection GetAttributes() => AttributeCollection.Empty;
			public string? GetClassName() => null;
			public string? GetComponentName() => null;
			public TypeConverter GetConverter() => new TypeConverter();
			public EventDescriptor? GetDefaultEvent() => null;
			public PropertyDescriptor? GetDefaultProperty() => null;
			public object? GetEditor(Type editorBaseType) => null;
			public EventDescriptorCollection GetEvents() => EventDescriptorCollection.Empty;
			public EventDescriptorCollection GetEvents(Attribute[]? attributes) => EventDescriptorCollection.Empty;

			public PropertyDescriptorCollection GetProperties()
			{
				GetPropertiesCalled.Value = true;
				return PropertyDescriptorCollection.Empty;
			}

			public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
			{
				throw new NotImplementedException();
			}

			public object? GetPropertyOwner(PropertyDescriptor? pd) => null;
		}
		public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object? instance)
		{
			Constructed.Value = true;
			return new CTD();
		}
	}

	[TypeDescriptionProvider(typeof(SomeTypeProvider))]
	public sealed class SomeType
	{
		public int SomeProperty { get; set; }
	}

	public static class Program
	{
		public static void ConcurrentTest(SomeType instance)
		{
			var properties = TypeDescriptor.GetProperties(instance);
			Console.WriteLine($"Constructed = {SomeTypeProvider.Constructed.Value} GetPropertiesCalled={SomeTypeProvider.GetPropertiesCalled.Value} properties.Count = {properties.Count} (should be 0)");
		}

		public static void Main()
		{
			const int threadCount = 10;
			// Uncomment and it will get the correct properties for all threads
			// TypeDescriptor.GetProperties(new SomeType());
			using (var finished = new CountdownEvent(threadCount))
			{
				SomeType[] instances = new SomeType[threadCount];
				for (var i = 0; i < threadCount; i++)
				{
					instances[i] = new SomeType();
				}
				for (var i = 0; i < threadCount; i++)
				{
					int _i = i;
					new Thread(() =>
					{
						ConcurrentTest(instances[_i]);
						finished.Signal();
					}).Start();
				}

				finished.Wait();
			}
		}
	}
}

### Expected behavior

Every thread should call the provider and get an empty list of properties.

### Actual behavior

Some threads will not call the provider and get the default properties based on reflection

### Regression?

Not that I know.

### Known Workarounds

_No response_

### Configuration

_No response_

### Other information

_No response_

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions