diff --git a/profiler/src/Demos/Samples.BuggyBits/Program.cs b/profiler/src/Demos/Samples.BuggyBits/Program.cs
index 7898680849a3..ad7d24fbcf27 100644
--- a/profiler/src/Demos/Samples.BuggyBits/Program.cs
+++ b/profiler/src/Demos/Samples.BuggyBits/Program.cs
@@ -54,33 +54,15 @@ public static async Task Main(string[] args)
ParseCommandLine(args, out _disableLogs, out var timeout, out var iterations, out var scenario, out var nbIdleThreads);
+ // Resolve the URL/port before building the host so that Kestrel is configured
+ // with a port that is actually free at bind time. This avoids a TOCTOU race:
+ // previously the host was built with the original --urls port, and the
+ // GetValidPort probe ran only after the build (too late to affect Kestrel).
+ args = ResolveListenUrl(args, out var rootUrl);
+ WriteLine($"Listening to {rootUrl}");
+
using (var host = CreateHostBuilder(args).Build())
{
- // ASP.NET Core accepts listening url via what is set by Visual Studio
- // (from the launchsettings.json). It could be overriden by --Urls
- // on the command line
- var configuration = host.Services.GetService(typeof(IConfiguration)) as IConfiguration;
- var rootUrl = configuration["urls"];
-
- // otherwise, use the default ASP.NET Core value
- if (string.IsNullOrEmpty(rootUrl))
- {
- rootUrl = "http://localhost:5000";
- }
-
- // avoid race condition in CI to find an available port
- int port = -1;
- if (int.TryParse(rootUrl.Substring(rootUrl.LastIndexOf(':') + 1), out port))
- {
- port = GetValidPort(port, 3);
- if (port != -1)
- {
- rootUrl = rootUrl.Substring(0, rootUrl.LastIndexOf(':') + 1) + port;
- }
- }
-
- WriteLine($"Listening to {rootUrl}");
-
var cts = new CancellationTokenSource();
using (var selfInvoker = new SelfInvoker(cts.Token, scenario, nbIdleThreads, _disableLogs))
{
@@ -177,46 +159,114 @@ public static int GetOpenPort()
}
}
- private static int GetValidPort(int initialPort, int retries)
+ ///
+ /// Resolves the Kestrel listen URL by finding a free port before the host is built.
+ /// This ensures receives the correct port in
+ /// so that Kestrel binds without a race.
+ ///
+ /// Original command-line args (may contain --urls).
+ /// The resolved URL with a free port substituted in.
+ /// Updated args array where --urls points to the resolved URL.
+ private static string[] ResolveListenUrl(string[] args, out string resolvedUrl)
{
- var port = initialPort;
- bool isPortValid = false;
- while (true)
+ // Extract the --urls value from command-line args (ASP.NET Core convention).
+ // Fall back to the default Kestrel URL if not specified.
+ string urlFromArgs = null;
+ for (int i = 0; i < args.Length - 1; i++)
{
- // seems like we can't reuse a listener if it fails to start,
- // so create a new listener each time we retry
- var listener = new HttpListener();
- listener.Prefixes.Add($"http://127.0.0.1:{port}/");
- listener.Prefixes.Add($"http://localhost:{port}/");
-
- try
+ if (args[i].Equals("--urls", StringComparison.OrdinalIgnoreCase))
{
- listener.Start();
-
- // success
- isPortValid = true;
+ urlFromArgs = args[i + 1];
break;
}
- catch (HttpListenerException) when (retries > 0)
- {
- // only catch the exception if there are retries left
- port = GetOpenPort();
- retries--;
- }
- finally
+ }
+
+ var baseUrl = urlFromArgs ?? "http://localhost:5000";
+
+ // Find a free port (up to 5 attempts with fresh ephemeral ports on each retry).
+ resolvedUrl = FindFreePortUrl(baseUrl, retries: 5);
+
+ // Replace (or inject) --urls so CreateHostBuilder configures Kestrel correctly.
+ return ReplaceUrlInArgs(args, resolvedUrl);
+ }
+
+ ///
+ /// Returns with its port component replaced by the first
+ /// available port found within attempts.
+ ///
+ private static string FindFreePortUrl(string baseUrl, int retries)
+ {
+ var lastColon = baseUrl.LastIndexOf(':');
+ if (lastColon < 0 || !int.TryParse(baseUrl.Substring(lastColon + 1), out int initialPort))
+ {
+ // No explicit port — return as-is and let Kestrel use its default.
+ return baseUrl;
+ }
+
+ var urlPrefix = baseUrl.Substring(0, lastColon + 1); // e.g. "http://localhost:"
+ var port = initialPort;
+
+ for (int attempt = 0; attempt <= retries; attempt++)
+ {
+ if (IsPortAvailable(port))
{
- listener.Close();
+ return urlPrefix + port;
}
+
+ // Port is busy — pick a fresh ephemeral port for the next attempt.
+ port = GetOpenPort();
}
- if (isPortValid)
+ // All retries exhausted — use the last candidate and surface a real error if
+ // it is still busy (better than silently starting on the wrong port).
+ return urlPrefix + port;
+ }
+
+ ///
+ /// Returns true if appears to be free on the loopback
+ /// interface; false if it is already in use.
+ ///
+ private static bool IsPortAvailable(int port)
+ {
+ var listener = new HttpListener();
+ listener.Prefixes.Add($"http://127.0.0.1:{port}/");
+ listener.Prefixes.Add($"http://localhost:{port}/");
+ try
{
- return port;
+ listener.Start();
+ return true;
}
- else
+ catch (HttpListenerException)
{
- return -1; // no valid port found
+ return false;
}
+ finally
+ {
+ listener.Close();
+ }
+ }
+
+ ///
+ /// Returns a copy of where the value after --urls is
+ /// replaced with . Appends --urls newUrl if the flag
+ /// is not already present.
+ ///
+ private static string[] ReplaceUrlInArgs(string[] args, string newUrl)
+ {
+ var list = new System.Collections.Generic.List(args);
+ for (int i = 0; i < list.Count - 1; i++)
+ {
+ if (list[i].Equals("--urls", StringComparison.OrdinalIgnoreCase))
+ {
+ list[i + 1] = newUrl;
+ return list.ToArray();
+ }
+ }
+
+ // "--urls" not found — append so Kestrel picks up the resolved port.
+ list.Add("--urls");
+ list.Add(newUrl);
+ return list.ToArray();
}
private static void ParseCommandLine(string[] args, out bool disableLogs, out TimeSpan timeout, out int iterations, out Scenario scenario, out int nbIdleThreads)