Skip to content

Commit 73b7222

Browse files
authored
fixing race in Binder that can cause NullReferenceExceptions (#3167)
1 parent f15e95c commit 73b7222

File tree

2 files changed

+96
-2
lines changed
  • src/Microsoft.Azure.WebJobs.Host/Bindings/Runtime
  • test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Runtime

2 files changed

+96
-2
lines changed

src/Microsoft.Azure.WebJobs.Host/Bindings/Runtime/Binder.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace Microsoft.Azure.WebJobs
1919
/// </summary>
2020
public class Binder : IBinder, IWatchable, IDisposable
2121
{
22+
private readonly object _bindersLock = new object();
2223
private readonly IAttributeBindingSource _bindingSource;
2324
private readonly IList<IValueBinder> _binders = new List<IValueBinder>();
2425
private readonly RuntimeBindingWatcher _watcher = new RuntimeBindingWatcher();
@@ -54,6 +55,11 @@ internal Binder(IAttributeBindingSource bindingSource)
5455
}
5556
}
5657

58+
/// <summary>
59+
/// For testing only.
60+
/// </summary>
61+
internal IList<IValueBinder> Binders => _binders;
62+
5763
/// <summary>
5864
/// Gets the binding data.
5965
/// </summary>
@@ -133,7 +139,10 @@ IWatcher IWatchable.Watcher
133139
IValueBinder binder = provider as IValueBinder;
134140
if (binder != null)
135141
{
136-
_binders.Add(binder);
142+
lock (_bindersLock)
143+
{
144+
_binders.Add(binder);
145+
}
137146
}
138147

139148
IDisposable disposableProvider = provider as IDisposable;
@@ -153,7 +162,13 @@ IWatcher IWatchable.Watcher
153162
/// <returns></returns>
154163
internal async Task Complete(CancellationToken cancellationToken)
155164
{
156-
foreach (IValueBinder binder in _binders)
165+
IValueBinder[] binders;
166+
lock (_bindersLock)
167+
{
168+
binders = _binders.ToArray();
169+
}
170+
171+
foreach (IValueBinder binder in binders)
157172
{
158173
// Binding can only be uses for non-Out parameters, and their binders ignore this argument.
159174
await binder.SetValueAsync(value: null, cancellationToken: cancellationToken);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.Azure.WebJobs.Host.Bindings;
9+
using Microsoft.Azure.WebJobs.Host.Bindings.Runtime;
10+
using Microsoft.Azure.WebJobs.Host.Protocols;
11+
using Moq;
12+
using Xunit;
13+
14+
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Bindings.Runtime;
15+
16+
public class BinderTests
17+
{
18+
[Fact]
19+
public async Task Complete_AfterConcurrentBinds_ThrowsNullReferenceException()
20+
{
21+
// Arrange
22+
var mockValueBinder = new Mock<IValueBinder>();
23+
mockValueBinder.Setup(m => m.Type).Returns(typeof(object));
24+
25+
var mockBinding = new Mock<IBinding>();
26+
mockBinding.Setup(b => b.BindAsync(It.IsAny<BindingContext>())).ReturnsAsync(mockValueBinder.Object);
27+
mockBinding.Setup(b => b.ToParameterDescriptor()).Returns(new ParameterDescriptor());
28+
29+
var mockBindingSource = new Mock<IAttributeBindingSource>();
30+
mockBindingSource.Setup(s => s.BindAsync<object>(It.IsAny<Attribute>(), It.IsAny<Attribute[]>(), It.IsAny<CancellationToken>()))
31+
.ReturnsAsync(mockBinding.Object);
32+
33+
var functionContext = new FunctionBindingContext(Guid.NewGuid(), CancellationToken.None, null);
34+
mockBindingSource.Setup(s => s.AmbientBindingContext).Returns(new AmbientBindingContext(functionContext, new Dictionary<string, object>().AsReadOnly()));
35+
36+
var binder = new Binder(mockBindingSource.Object);
37+
var threads = new List<Thread>();
38+
39+
const int numConcurrentThreads = 100;
40+
const int numTasksPerThread = 100;
41+
42+
// Call BindAsync simultaneously from many threads. There was a race where this would corrupt
43+
// the internal _binders list.
44+
for (int i = 0; i < numConcurrentThreads; i++)
45+
{
46+
threads.Add(new Thread(() =>
47+
{
48+
var tasks = new List<Task>();
49+
for (int t = 0; t < numTasksPerThread; t++)
50+
{
51+
tasks.Add(binder.BindAsync<object>(new TestAttribute()));
52+
}
53+
54+
Task.WaitAll(tasks.ToArray());
55+
}));
56+
}
57+
58+
foreach (var thread in threads)
59+
{
60+
thread.Start();
61+
}
62+
63+
foreach (var thread in threads)
64+
{
65+
thread.Join();
66+
}
67+
68+
// Now that all concurrent binds are done, call Complete.
69+
// If the list's internal state was corrupted by the concurrent Add calls,
70+
// this iteration will likely hit a null element.
71+
await binder.Complete(CancellationToken.None);
72+
73+
// Because List resizing is not deterministic, another side effect is having a
74+
// different count of binders than expected.
75+
Assert.Equal(numConcurrentThreads * numTasksPerThread, binder.Binders.Count);
76+
}
77+
78+
private class TestAttribute : Attribute { }
79+
}

0 commit comments

Comments
 (0)