sdk-container-builds
sdk-container-builds copied to clipboard
Freeze when ContainerImageName contains an uppercase letter
Summary
In version 0.3.2 of the Microsoft.Net.Build.Containers nuget, if you use a long enough ContainerImageName property that contains an uppercase letter, the publishing process will seemingly freeze. The root cause is there is an exponential blowup in the regex used by Microsoft.NET.Build.Containers.ContainerHelpers.IsValidImageName.
Workaround
Remove uppercase characters from the ContainerImageName property. I don't think they are valid anyways.
As concrete example, I was trying to publish an image named us-central1-docker.pkg.dev/my-project-id/sandwich-apps/SandwichTracker. Changing it to us-central1-docker.pkg.dev/my-project-id/sandwich-apps/sandwichtracker allowed the publishing process to finish in a reasonable amount of time.
Details
See this example app, referencing the %USERPROFILE%\.nuget\packages\microsoft.net.build.containers\0.3.2\tasks\net7.0\Microsoft.NET.Build.Containers.dll assembly:
using System.Diagnostics;
testRegex("aaaaaaaaaaaaaaaaaa");
testRegex("aaaaaaaaaaaaaaaaAa");
testRegex("aaaaaaaaaaaaaaaaaaAa");
testRegex("aaaaaaaaaaaaaaaaaaaAa");
testRegex("aaaaaaaaaaaaaaaaaaaaAa");
testRegex("aaaaaaaaaaaaaaaaaaaaaAa");
testRegex("aaaaaaaaaaaaaaaaaaaaaaAa");
void testRegex(string input)
{
var sw = Stopwatch.StartNew();
bool isMatch = Microsoft.NET.Build.Containers.ContainerHelpers.IsValidImageName(input);
sw.Stop();
Console.WriteLine($"{input}: {isMatch}, took {sw.ElapsedMilliseconds} ms");
}
Output:
aaaaaaaaaaaaaaaaaa: True, took 4 ms
aaaaaaaaaaaaaaaaAa: False, took 10 ms
aaaaaaaaaaaaaaaaaaAa: False, took 41 ms
aaaaaaaaaaaaaaaaaaaAa: False, took 81 ms
aaaaaaaaaaaaaaaaaaaaAa: False, took 158 ms
aaaaaaaaaaaaaaaaaaaaaAa: False, took 319 ms
aaaaaaaaaaaaaaaaaaaaaaAa: False, took 632 ms
Stack trace of where the publish process gets stuck:
System.Text.RegularExpressions.dll!System.Text.RegularExpressions.RegexInterpreter.TryMatchAtCurrentPosition(System.ReadOnlySpan<char> inputSpan) Line 413 C#
System.Text.RegularExpressions.dll!System.Text.RegularExpressions.RegexInterpreter.Scan(System.ReadOnlySpan<char> text) Line 357 C#
> System.Text.RegularExpressions.dll!System.Text.RegularExpressions.Regex.RunSingleMatch(System.Text.RegularExpressions.RegexRunnerMode mode, int prevlen, string input, int beginning, int length, int startat) Line 413 C#
System.Text.RegularExpressions.dll!System.Text.RegularExpressions.Regex.IsMatch(string input) Line 80 C#
Microsoft.NET.Build.Containers.dll!Microsoft.NET.Build.Containers.ContainerHelpers.IsValidImageName(string imageName) Line 61 C#
Microsoft.NET.Build.Containers.dll!Microsoft.NET.Build.Containers.ContainerHelpers.NormalizeImageName(string containerImageName, out string normalizedImageName) Line 187 C#
Microsoft.NET.Build.Containers.dll!Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.Execute() Line 161 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute() Line 570 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask(Microsoft.Build.BackEnd.ITaskExecutionHost taskExecutionHost, Microsoft.Build.BackEnd.Logging.TaskLoggingContext taskLoggingContext, Microsoft.Build.BackEnd.TaskHost taskHost, Microsoft.Build.BackEnd.ItemBucket bucket, Microsoft.Build.BackEnd.TaskExecutionMode howToExecuteTask) Line 826 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TaskBuilder.InitializeAndExecuteTask(Microsoft.Build.BackEnd.Logging.TaskLoggingContext taskLoggingContext, Microsoft.Build.BackEnd.ItemBucket bucket, System.Collections.Generic.IDictionary<string, string> taskIdentityParameters, Microsoft.Build.BackEnd.TaskHost taskHost, Microsoft.Build.BackEnd.TaskExecutionMode howToExecuteTask) Line 674 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TaskBuilder.ExecuteBucket(Microsoft.Build.BackEnd.TaskHost taskHost, Microsoft.Build.BackEnd.ItemBucket bucket, Microsoft.Build.BackEnd.TaskExecutionMode howToExecuteTask, System.Collections.Generic.Dictionary<string, string> lookupHash) Line 455 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TaskBuilder.ExecuteTask(Microsoft.Build.BackEnd.TaskExecutionMode mode, Microsoft.Build.BackEnd.Lookup lookup) Line 331 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TaskBuilder.ExecuteTask(Microsoft.Build.BackEnd.Logging.TargetLoggingContext loggingContext, Microsoft.Build.BackEnd.BuildRequestEntry requestEntry, Microsoft.Build.BackEnd.ITargetBuilderCallback targetBuilderCallback, Microsoft.Build.Execution.ProjectTargetInstanceChild taskInstance, Microsoft.Build.BackEnd.TaskExecutionMode mode, Microsoft.Build.BackEnd.Lookup inferLookup, Microsoft.Build.BackEnd.Lookup executeLookup, System.Threading.CancellationToken cancellationToken) Line 185 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TargetEntry.ProcessBucket(Microsoft.Build.BackEnd.ITaskBuilder taskBuilder, Microsoft.Build.BackEnd.Logging.TargetLoggingContext targetLoggingContext, Microsoft.Build.BackEnd.TaskExecutionMode mode, Microsoft.Build.BackEnd.Lookup lookupForInference, Microsoft.Build.BackEnd.Lookup lookupForExecution) Line 808 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TargetEntry.ExecuteTarget(Microsoft.Build.BackEnd.ITaskBuilder taskBuilder, Microsoft.Build.BackEnd.BuildRequestEntry requestEntry, Microsoft.Build.BackEnd.Logging.ProjectLoggingContext projectLoggingContext, System.Threading.CancellationToken cancellationToken) Line 495 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.TargetBuilder.ProcessTargetStack(Microsoft.Build.BackEnd.ITaskBuilder taskBuilder) Line 489 C#
[Resuming Async Method]
System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.AsyncStateMachineBox<Microsoft.Build.BackEnd.TargetBuilder.<ProcessTargetStack>d__23>.ExecutionContextCallback(object s) Line 287 C#
System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) Line 183 C#
System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.AsyncStateMachineBox<Microsoft.Build.BackEnd.TargetBuilder.<ProcessTargetStack>d__23>.MoveNext(System.Threading.Thread threadPoolThread) Line 324 C#
System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.AsyncStateMachineBox<Microsoft.Build.BackEnd.TargetBuilder.<ProcessTargetStack>d__23>.MoveNext() Line 302 C#
System.Private.CoreLib.dll!System.Threading.Tasks.TaskSchedulerAwaitTaskContinuation.Run.AnonymousMethod__2_0(object state) Line 497 C#
System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot, System.Threading.Thread threadPoolThread) Line 2338 C#
System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteEntry() Line 2265 C#
Microsoft.Build.dll!Microsoft.Build.BackEnd.RequestBuilder.DedicatedThreadsTaskScheduler.InjectThread.AnonymousMethod__6_0() Line 1436 C#
System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) Line 183 C#
[Async Call Stack]
[Async] Microsoft.Build.dll!Microsoft.Build.BackEnd.TargetBuilder.BuildTargets(Microsoft.Build.BackEnd.Logging.ProjectLoggingContext loggingContext, Microsoft.Build.BackEnd.BuildRequestEntry entry, Microsoft.Build.BackEnd.IRequestBuilderCallback callback, string[] targetNames, Microsoft.Build.BackEnd.Lookup baseLookup, System.Threading.CancellationToken cancellationToken) Line 168 C#
[Async] Microsoft.Build.dll!Microsoft.Build.BackEnd.RequestBuilder.BuildProject() Line 1188 C#
[Async] Microsoft.Build.dll!Microsoft.Build.BackEnd.RequestBuilder.BuildAndReport() Line 808 C#
[Async] Microsoft.Build.dll!Microsoft.Build.BackEnd.RequestBuilder.RequestThreadProc(bool setThreadParameters) Line 773 C#
[Async] System.Private.CoreLib.dll!System.Threading.Tasks.Task.Run C#
For reference, this is the offending regex:
^(?:((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?|\[(?:[a-fA-F0-9:]+)])(?::[0-9]+)?)/)?([a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)$
Thank you for the report! I plugged the regex into a few regex testers and they did in fact find 'catastrophic backtracking', which is maybe the best way I've heard that described before. The regexes we use were ported mostly from the docker source in the golang style so we could verify them, but perhaps there are behaviors we need to configure that don't translate well.
I think the reason Go did not have a problem with the regex is it uses RE2, which does not support backtracking. Dotnet can also run this regex quickly with RegexOptions.NonBacktracking. Unfortunately it's not available on .NET Framework.