From 13453892b4a776916b9943a1c1d756611d23e1ce Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Tue, 19 May 2026 07:58:08 -0700 Subject: [PATCH 1/4] Add configurable debuginfod settings to prevent GDB hangs ## Why is this change being made? Fixes https://github.com/microsoft/vscode-cpptools/issues/14458 MIEngine commit f169b02 added `set debuginfod enabled on` unconditionally in GetInitializeCommands(). On systems where DEBUGINFOD_URLS points to an unreachable server (e.g. debuginfod.ubuntu.com which is currently down), subsequent GDB commands block for 90+ seconds waiting for the network request to time out, causing GDB to appear hung on launch. ## Summarize what changed - Added a `debuginfod` launch option (JSON: `{ enabled: bool, timeout: int }` XML: `EnableDebuginfod`/`DebuginfodTimeout` attributes) defaulting to enabled with a 30-second timeout. - When enabled (timeout > 0): sets DEBUGINFOD_TIMEOUT and DEBUGINFOD_MAXTIME environment variables on the GDB process to cap network requests. - When enabled (timeout = 0): no timeout override is applied; GDB/libdebuginfod defaults are used. - When disabled: clears DEBUGINFOD_URLS on the GDB process so debuginfod connections are never attempted, even if the system has the env var set. - `set debuginfod enabled on` is now conditional on the EnableDebuginfod setting. - Added XSD schema validation (minInclusive=0) and JSON schema (minimum: 0). - Negative timeout values are clamped to the default (30) at runtime. ## How was the change tested? - [x] Changes reviewed by Copilot CLI (using /review) - [x] All 36 MICoreUnitTests pass - [x] Integration tests (DebuginfodTests) pass in WSL with GDB 12.1: - DebuginfodDisabledDoesNotHang: steps into std::regex with unreachable server, verifies launch completes in < 10s - DebuginfodTimeoutPreventsHang: steps into std::regex with 5s timeout, verifies launch completes in < 30s - [x] All existing CppTests pass (Sample, Execution, Breakpoint, Environment) - [x] Verified via TCP listener that debuginfod URLs are actually queried during step-into-library operations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MICore/JsonLaunchOptions.cs | 21 ++ src/MICore/LaunchOptions.cs | 35 ++++ src/MICore/LaunchOptions.xsd | 22 ++ .../LaunchOptions.xsd.types.designer.cs | 36 ++++ src/MICore/Transports/LocalTransport.cs | 19 ++ .../Engine.Impl/DebuggedProcess.cs | 5 +- src/MIDebugPackage/OpenFolderSchema.json | 18 ++ .../OpenDebug/CrossPlatCpp/LaunchCommand.cs | 27 +++ test/CppTests/Tests/DebuggeeMonikers.cs | 5 + test/CppTests/Tests/DebuginfodTests.cs | 192 ++++++++++++++++++ .../debuginfod/src/debuginfod_test.cpp | 29 +++ 11 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 test/CppTests/Tests/DebuginfodTests.cs create mode 100644 test/CppTests/debuggees/debuginfod/src/debuginfod_test.cpp diff --git a/src/MICore/JsonLaunchOptions.cs b/src/MICore/JsonLaunchOptions.cs index 6277a5633..6f872157e 100644 --- a/src/MICore/JsonLaunchOptions.cs +++ b/src/MICore/JsonLaunchOptions.cs @@ -122,6 +122,12 @@ public abstract partial class BaseOptions /// [JsonProperty("unknownBreakpointHandling", DefaultValueHandling = DefaultValueHandling.Ignore)] public UnknownBreakpointHandling? UnknownBreakpointHandling { get; set; } + + /// + /// Controls GDB's debuginfod behavior. + /// + [JsonProperty("debuginfod", DefaultValueHandling = DefaultValueHandling.Ignore)] + public DebuginfodSettings Debuginfod { get; set; } } internal class VisualizerFileConverter : JsonConverter @@ -315,6 +321,21 @@ public enum UnknownBreakpointHandling Stop } + public partial class DebuginfodSettings + { + /// + /// If true (default), GDB's debuginfod support is enabled. + /// + [JsonProperty("enabled")] + public bool? Enabled { get; set; } + + /// + /// The timeout in seconds for debuginfod server requests. Default is 30. Set to 0 for no override. + /// + [JsonProperty("timeout", DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? Timeout { get; set; } + } + public partial class LaunchOptions : BaseOptions { #region Public Properties for Serialization diff --git a/src/MICore/LaunchOptions.cs b/src/MICore/LaunchOptions.cs index dbe934817..2c9b6a95e 100644 --- a/src/MICore/LaunchOptions.cs +++ b/src/MICore/LaunchOptions.cs @@ -1211,6 +1211,36 @@ public UnknownBreakpointHandling UnknownBreakpointHandling } } + private bool _enableDebuginfod = true; + + /// + /// If true (default), GDB's debuginfod support is enabled. + /// + public bool EnableDebuginfod + { + get { return _enableDebuginfod; } + set + { + VerifyCanModifyProperty(nameof(EnableDebuginfod)); + _enableDebuginfod = value; + } + } + + private int _debuginfodTimeout = 30; + + /// + /// The timeout in seconds for debuginfod requests. Default is 30. Set to 0 for no override. + /// + public int DebuginfodTimeout + { + get { return _debuginfodTimeout; } + set + { + VerifyCanModifyProperty(nameof(DebuginfodTimeout)); + _debuginfodTimeout = value; + } + } + public string GetOptionsString() { try @@ -1824,6 +1854,9 @@ protected void InitializeCommonOptions(Json.LaunchOptions.BaseOptions options) } this.UnknownBreakpointHandling = options.UnknownBreakpointHandling ?? UnknownBreakpointHandling.Throw; + this.EnableDebuginfod = options.Debuginfod?.Enabled ?? true; + int debuginfodTimeout = options.Debuginfod?.Timeout ?? 30; + this.DebuginfodTimeout = debuginfodTimeout >= 0 ? debuginfodTimeout : 30; } protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions source) @@ -1855,6 +1888,8 @@ protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions sourc this.ShowDisplayString = source.ShowDisplayString; this.WaitDynamicLibLoad = source.WaitDynamicLibLoad; + this.EnableDebuginfod = source.EnableDebuginfod; + this.DebuginfodTimeout = source.DebuginfodTimeout >= 0 ? source.DebuginfodTimeout : 30; this.SetupCommands = LaunchCommand.CreateCollection(source.SetupCommands); this.PostRemoteConnectCommands = LaunchCommand.CreateCollection(source.PostRemoteConnectCommands); diff --git a/src/MICore/LaunchOptions.xsd b/src/MICore/LaunchOptions.xsd index db1f833a9..cae1c94d8 100644 --- a/src/MICore/LaunchOptions.xsd +++ b/src/MICore/LaunchOptions.xsd @@ -207,6 +207,28 @@ + + + + If true (default), GDB's debuginfod support is enabled, allowing automatic downloading of debug symbols. + Set to false to disable debuginfod, which can prevent GDB from hanging when debuginfod servers are unavailable. + + + + + + + The timeout in seconds for debuginfod server requests. Default is 30. Only applies when EnableDebuginfod is true. + A value of 0 means no timeout override is applied (GDB/libdebuginfod defaults are used). + This sets the DEBUGINFOD_TIMEOUT and DEBUGINFOD_MAXTIME environment variables on the GDB process. + + + + + + + + diff --git a/src/MICore/LaunchOptions.xsd.types.designer.cs b/src/MICore/LaunchOptions.xsd.types.designer.cs index 0c0c83941..ab1b8c954 100644 --- a/src/MICore/LaunchOptions.xsd.types.designer.cs +++ b/src/MICore/LaunchOptions.xsd.types.designer.cs @@ -99,12 +99,24 @@ public partial class AndroidLaunchOptions { [System.ComponentModel.DefaultValueAttribute(true)] public bool WaitDynamicLibLoad; + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(true)] + public bool EnableDebuginfod; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(30)] + public int DebuginfodTimeout; + public AndroidLaunchOptions() { this.Attach = false; this.SourceRoots = ""; this.JVMPort = 65534; this.JVMHost = "localhost"; this.WaitDynamicLibLoad = true; + this.EnableDebuginfod = true; + this.DebuginfodTimeout = 30; } } @@ -454,8 +466,20 @@ public partial class BaseLaunchOptions { [System.ComponentModel.DefaultValueAttribute(true)] public bool WaitDynamicLibLoad; + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(true)] + public bool EnableDebuginfod; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(30)] + public int DebuginfodTimeout; + public BaseLaunchOptions() { this.WaitDynamicLibLoad = true; + this.EnableDebuginfod = true; + this.DebuginfodTimeout = 30; } } @@ -536,8 +560,20 @@ public partial class IOSLaunchOptions { [System.ComponentModel.DefaultValueAttribute(true)] public bool WaitDynamicLibLoad; + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(true)] + public bool EnableDebuginfod; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(30)] + public int DebuginfodTimeout; + public IOSLaunchOptions() { this.WaitDynamicLibLoad = true; + this.EnableDebuginfod = true; + this.DebuginfodTimeout = 30; } } diff --git a/src/MICore/Transports/LocalTransport.cs b/src/MICore/Transports/LocalTransport.cs index 71e6f0aa3..2fdecc932 100755 --- a/src/MICore/Transports/LocalTransport.cs +++ b/src/MICore/Transports/LocalTransport.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text; using System.Threading; using System.Diagnostics; @@ -40,6 +41,24 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, proc.StartInfo.SetEnvironmentVariable("PATH", path); } + // Configure debuginfod environment variables on the GDB process. + if (options.DebuggerMIMode == MIMode.Gdb) + { + if (options.EnableDebuginfod) + { + if (options.DebuginfodTimeout > 0) + { + string timeoutStr = options.DebuginfodTimeout.ToString(CultureInfo.InvariantCulture); + proc.StartInfo.SetEnvironmentVariable("DEBUGINFOD_TIMEOUT", timeoutStr); + proc.StartInfo.SetEnvironmentVariable("DEBUGINFOD_MAXTIME", timeoutStr); + } + } + else + { + proc.StartInfo.SetEnvironmentVariable("DEBUGINFOD_URLS", ""); + } + } + // Allow to execute custom commands before launching debugger. // For ex., instructing GDB not to break for certain signals if (options.DebuggerMIMode == MIMode.Gdb && !string.IsNullOrWhiteSpace(options.WorkingDirectory)) diff --git a/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs b/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs index 942a366dd..b5e8d1bd3 100755 --- a/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs +++ b/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs @@ -624,7 +624,10 @@ private async Task> GetInitializeCommands() if (_launchOptions.DebuggerMIMode == MIMode.Gdb) { commands.Add(new LaunchCommand("-interpreter-exec console \"set pagination off\"")); - commands.Add(new LaunchCommand("set debuginfod enabled on", ignoreFailures:true)); + if (_launchOptions.EnableDebuginfod) + { + commands.Add(new LaunchCommand("set debuginfod enabled on", ignoreFailures: true)); + } } // When user specifies loading directives then the debugger cannot auto load symbols, the MIEngine must intervene at each solib-load event and make a determination diff --git a/src/MIDebugPackage/OpenFolderSchema.json b/src/MIDebugPackage/OpenFolderSchema.json index e8431f00f..44f473dc4 100644 --- a/src/MIDebugPackage/OpenFolderSchema.json +++ b/src/MIDebugPackage/OpenFolderSchema.json @@ -193,6 +193,24 @@ "stop" ], "description": "Controls how breakpoints set externally (usually via raw GDB commands) are handled when hit.\nAllowed values are \"throw\", which acts as if an exception was thrown by the application, and \"stop\", which only pauses the debug session. The default value is \"throw\"." + }, + "debuginfod": { + "type": "object", + "description": "Controls GDB's debuginfod behavior for automatic downloading of debug symbols.", + "default": { "enabled": true, "timeout": 30 }, + "properties": { + "enabled": { + "type": "boolean", + "description": "If true (default), GDB's debuginfod support is enabled. Set to false to disable debuginfod, which can prevent GDB from hanging when debuginfod servers are unavailable.", + "default": true + }, + "timeout": { + "type": "integer", + "description": "The timeout in seconds for debuginfod server requests. Default is 30. Set to 0 for no timeout override (GDB defaults apply).", + "default": 30, + "minimum": 0 + } + } } }, "definitions": { diff --git a/test/CppTests/OpenDebug/CrossPlatCpp/LaunchCommand.cs b/test/CppTests/OpenDebug/CrossPlatCpp/LaunchCommand.cs index 06e37fb21..5da394a34 100644 --- a/test/CppTests/OpenDebug/CrossPlatCpp/LaunchCommand.cs +++ b/test/CppTests/OpenDebug/CrossPlatCpp/LaunchCommand.cs @@ -40,6 +40,33 @@ public sealed class CppLaunchCommandArgs : LaunchCommandArgs [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public bool ShowDisplayString; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public DebuginfodArgs debuginfod; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public SetupCommandArg[] setupCommands; + } + + public sealed class DebuginfodArgs + { + [JsonProperty("enabled", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? Enabled { get; set; } + + [JsonProperty("timeout", DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? Timeout { get; set; } + } + + public sealed class SetupCommandArg + { + [JsonProperty("text")] + public string Text { get; set; } + + [JsonProperty("description", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Description { get; set; } + + [JsonProperty("ignoreFailures", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool? IgnoreFailures { get; set; } } #endregion diff --git a/test/CppTests/Tests/DebuggeeMonikers.cs b/test/CppTests/Tests/DebuggeeMonikers.cs index 6738f4c99..c49a53408 100644 --- a/test/CppTests/Tests/DebuggeeMonikers.cs +++ b/test/CppTests/Tests/DebuggeeMonikers.cs @@ -60,5 +60,10 @@ internal static class Natvis { public const int Default = 1; } + + internal static class Debuginfod + { + public const int Default = 1; + } } } diff --git a/test/CppTests/Tests/DebuginfodTests.cs b/test/CppTests/Tests/DebuginfodTests.cs new file mode 100644 index 000000000..b14dae27b --- /dev/null +++ b/test/CppTests/Tests/DebuginfodTests.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using DebuggerTesting; +using DebuggerTesting.Compilation; +using DebuggerTesting.OpenDebug; +using DebuggerTesting.OpenDebug.Commands; +using DebuggerTesting.OpenDebug.CrossPlatCpp; +using DebuggerTesting.OpenDebug.Events; +using DebuggerTesting.OpenDebug.Extensions; +using DebuggerTesting.Ordering; +using DebuggerTesting.Settings; +using Xunit; +using Xunit.Abstractions; + +namespace CppTests.Tests +{ + /// + /// Tests that validate debuginfod settings don't cause GDB to hang when + /// debuginfod servers are unreachable. + /// + [TestCaseOrderer(DependencyTestOrderer.TypeName, DependencyTestOrderer.AssemblyName)] + public class DebuginfodTests : TestBase + { + #region Constructor + + public DebuginfodTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + #endregion + + private const string DebuggeeName = "debuginfod"; + private const string SourceFileName = "debuginfod_test.cpp"; + + // RFC 5737 TEST-NET-1: guaranteed non-routable, connections will be dropped + private const string UnreachableDebuginfodUrl = "http://192.0.2.1:8002"; + + #region Tests + + [Theory] + [RequiresTestSettings] + public void CompileDebuginfodDebuggee(ITestSettings settings) + { + this.TestPurpose("Create and compile the debuginfod test debuggee"); + this.WriteSettings(settings); + + IDebuggee debuggee = Debuggee.Create(this, settings.CompilerSettings, DebuggeeName, DebuggeeMonikers.Debuginfod.Default); + debuggee.AddSourceFiles(SourceFileName); + debuggee.Compile(); + } + + /// + /// Tests that disabling debuginfod prevents hangs when stepping into library code + /// with DEBUGINFOD_URLS pointing to an unreachable server. + /// + [Theory] + [DependsOnTest(nameof(CompileDebuginfodDebuggee))] + [RequiresTestSettings] + [UnsupportedDebugger(SupportedDebugger.VsDbg | SupportedDebugger.Lldb, SupportedArchitecture.x64 | SupportedArchitecture.x86)] + public void DebuginfodDisabledDoesNotHang(ITestSettings settings) + { + this.TestPurpose("Verify that disabling debuginfod prevents GDB from hanging when stepping into library code."); + this.WriteSettings(settings); + + IDebuggee debuggee = Debuggee.Open(this, settings.CompilerSettings, DebuggeeName, DebuggeeMonikers.Debuginfod.Default); + + string originalUrl = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS"); + string originalVerbose = Environment.GetEnvironmentVariable("DEBUGINFOD_VERBOSE"); + try + { + Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", UnreachableDebuginfodUrl); + Environment.SetEnvironmentVariable("DEBUGINFOD_VERBOSE", "1"); + + Stopwatch sw = Stopwatch.StartNew(); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Launch with debuginfod disabled"); + LaunchCommand launch = new LaunchCommand(settings.DebuggerSettings, debuggee.OutputPath); + launch.Args.debuginfod = new DebuginfodArgs { Enabled = false }; + runner.RunCommand(launch); + + this.Comment("Set breakpoint at regex_search call and run to it"); + runner.SetBreakpoints(debuggee.Breakpoints(SourceFileName, 13)); + runner.Expects.StoppedEvent(StoppedReason.Breakpoint).AfterConfigurationDone(); + + this.Comment("Step into std::regex_search to trigger debuginfod lookup"); + runner.Expects.StoppedEvent(StoppedReason.Step).AfterStepIn(); + + sw.Stop(); + this.Comment($"Step into library completed in {sw.ElapsedMilliseconds}ms"); + + Assert.True(sw.ElapsedMilliseconds < 10000, + $"Step into library took {sw.ElapsedMilliseconds}ms with debuginfod disabled. " + + "Expected < 10s. Debuginfod may not be properly disabled."); + + runner.Expects.ExitedEvent().TerminatedEvent().AfterContinue(); + runner.DisconnectAndVerify(); + } + + string engineLogPath = Path.Combine(PathSettings.TempPath, + $"EngineLog-{nameof(DebuginfodDisabledDoesNotHang)}-{settings.DebuggerSettings.DebuggeeArchitecture}-{settings.DebuggerSettings.DebuggerType}.log"); + if (File.Exists(engineLogPath)) + { + string logContent = File.ReadAllText(engineLogPath); + this.Comment("Verifying debuginfod was NOT enabled in GDB"); + Assert.DoesNotContain("debuginfod enabled", logContent); + } + } + finally + { + Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalUrl); + Environment.SetEnvironmentVariable("DEBUGINFOD_VERBOSE", originalVerbose); + } + } + + /// + /// Tests that a short debuginfod timeout prevents hangs when stepping into library code + /// with DEBUGINFOD_URLS pointing to an unreachable server. + /// + [Theory] + [DependsOnTest(nameof(CompileDebuginfodDebuggee))] + [RequiresTestSettings] + [UnsupportedDebugger(SupportedDebugger.VsDbg | SupportedDebugger.Lldb | SupportedDebugger.Gdb_MinGW | SupportedDebugger.Gdb_Cygwin, SupportedArchitecture.x64 | SupportedArchitecture.x86)] + public void DebuginfodTimeoutPreventsHang(ITestSettings settings) + { + this.TestPurpose("Verify that debuginfod timeout prevents GDB from hanging when stepping into library code."); + this.WriteSettings(settings); + + IDebuggee debuggee = Debuggee.Open(this, settings.CompilerSettings, DebuggeeName, DebuggeeMonikers.Debuginfod.Default); + + string originalUrl = Environment.GetEnvironmentVariable("DEBUGINFOD_URLS"); + string originalVerbose = Environment.GetEnvironmentVariable("DEBUGINFOD_VERBOSE"); + try + { + Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", UnreachableDebuginfodUrl); + Environment.SetEnvironmentVariable("DEBUGINFOD_VERBOSE", "1"); + + Stopwatch sw = Stopwatch.StartNew(); + + using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings)) + { + this.Comment("Launch with debuginfod enabled but short timeout (5s)"); + LaunchCommand launch = new LaunchCommand(settings.DebuggerSettings, debuggee.OutputPath); + launch.Args.debuginfod = new DebuginfodArgs { Enabled = true, Timeout = 5 }; + launch.Args.setupCommands = new SetupCommandArg[] + { + new SetupCommandArg { Text = "set debuginfod verbose 1", IgnoreFailures = true } + }; + runner.RunCommand(launch); + + this.Comment("Set breakpoint at regex_search call and run to it"); + runner.SetBreakpoints(debuggee.Breakpoints(SourceFileName, 13)); + runner.Expects.StoppedEvent(StoppedReason.Breakpoint).AfterConfigurationDone(); + + this.Comment("Step into std::regex_search to trigger debuginfod lookup"); + runner.Expects.StoppedEvent(StoppedReason.Step).AfterStepIn(); + + sw.Stop(); + this.Comment($"Step into library completed in {sw.ElapsedMilliseconds}ms"); + + Assert.True(sw.ElapsedMilliseconds < 30000, + $"Step into library took {sw.ElapsedMilliseconds}ms with debuginfod timeout=5s. " + + "Expected < 30s. The timeout may not be applied correctly."); + + runner.Expects.ExitedEvent().TerminatedEvent().AfterContinue(); + runner.DisconnectAndVerify(); + } + + string engineLogPath = Path.Combine(PathSettings.TempPath, + $"EngineLog-{nameof(DebuginfodTimeoutPreventsHang)}-{settings.DebuggerSettings.DebuggeeArchitecture}-{settings.DebuggerSettings.DebuggerType}.log"); + if (File.Exists(engineLogPath)) + { + string logContent = File.ReadAllText(engineLogPath); + this.Comment("Verifying debuginfod was enabled in GDB"); + Assert.Contains("debuginfod enabled", logContent); + } + } + finally + { + Environment.SetEnvironmentVariable("DEBUGINFOD_URLS", originalUrl); + Environment.SetEnvironmentVariable("DEBUGINFOD_VERBOSE", originalVerbose); + } + } + + #endregion + } +} diff --git a/test/CppTests/debuggees/debuginfod/src/debuginfod_test.cpp b/test/CppTests/debuggees/debuginfod/src/debuginfod_test.cpp new file mode 100644 index 000000000..396682c92 --- /dev/null +++ b/test/CppTests/debuggees/debuginfod/src/debuginfod_test.cpp @@ -0,0 +1,29 @@ +// Test program that calls into library functions where debuginfod would +// attempt to download debug symbols/source. When stepping through regex +// or libc internals, GDB triggers debuginfod lookups if enabled. + +#include +#include +#include + +int do_regex_match(const std::string& input, const std::string& pattern) +{ + std::regex re(pattern); + std::smatch match; + if (std::regex_search(input, match, re)) + { + std::cout << "Match: " << match[0] << std::endl; + return 1; // breakpoint line + } + return 0; +} + +int main() +{ + std::string text = "Hello debuginfod test 12345"; + std::string pattern = R"(\d+)"; + + int result = do_regex_match(text, pattern); // step into here + std::cout << "Result: " << result << std::endl; + return result; +} From 2a61faa8c21a0759ba8ce94c4e2b0aac1ffc2089 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Tue, 19 May 2026 13:03:32 -0700 Subject: [PATCH 2/4] Add other transports --- src/MICore/LaunchOptions.cs | 87 +++++++++++++++++-- src/MICore/Transports/LocalTransport.cs | 18 +--- src/MICore/Transports/PipeTransport.cs | 5 ++ .../Transports/RunInTerminalTransport.cs | 3 +- 4 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/MICore/LaunchOptions.cs b/src/MICore/LaunchOptions.cs index 2c9b6a95e..f05bb7fe9 100644 --- a/src/MICore/LaunchOptions.cs +++ b/src/MICore/LaunchOptions.cs @@ -137,15 +137,17 @@ static internal PipeLaunchOptions CreateFromJson(JObject parsedOptions) quoteArgs = platformSpecificTransportOptions.QuoteArgs ?? quoteArgs; } + Json.LaunchOptions.BaseOptions baseOptions = Json.LaunchOptions.LaunchOptionHelpers.GetLaunchOrAttachOptions(parsedOptions); + string debuginfodPrefix = ComputeDebuginfodPrefixFromSettings(baseOptions.Debuginfod); + PipeLaunchOptions pipeOptions = new PipeLaunchOptions( pipePath: pipeProgram, - pipeArguments: EnsurePipeArguments(pipeArgs, debuggerPath, gdbPathDefault, quoteArgs), + pipeArguments: EnsurePipeArguments(pipeArgs, debuggerPath, gdbPathDefault, quoteArgs, debuginfodPrefix), pipeCommandArguments: ParseArguments(pipeCmd??pipeArgs, quoteArgs), pipeCwd: pipeCwd, pipeEnvironment: GetEnvironmentEntries(pipeEnv) ); - Json.LaunchOptions.BaseOptions baseOptions = Json.LaunchOptions.LaunchOptionHelpers.GetLaunchOrAttachOptions(parsedOptions); pipeOptions.InitializeCommonOptions(baseOptions); if (baseOptions is Json.LaunchOptions.LaunchOptions) { @@ -160,13 +162,30 @@ static internal PipeLaunchOptions CreateFromJson(JObject parsedOptions) return pipeOptions; } - private static string EnsurePipeArguments(List pipeArgs, string debuggerPath, string debuggerPathDefault, bool quoteArgs) + private static string ComputeDebuginfodPrefixFromSettings(Json.LaunchOptions.DebuginfodSettings settings) + { + bool enabled = settings?.Enabled ?? true; + int timeout = settings?.Timeout ?? 30; + if (timeout < 0) timeout = 30; + + if (enabled && timeout > 0) + { + return string.Format(CultureInfo.InvariantCulture, "env DEBUGINFOD_TIMEOUT={0} DEBUGINFOD_MAXTIME={0} ", timeout); + } + else if (!enabled) + { + return "env DEBUGINFOD_URLS= "; + } + return string.Empty; + } + + private static string EnsurePipeArguments(List pipeArgs, string debuggerPath, string debuggerPathDefault, bool quoteArgs, string debuginfodPrefix = "") { // Debugger path. Assume /usr/bin/gdb unless specified string dbgPath = String.IsNullOrWhiteSpace(debuggerPath) ? debuggerPathDefault : debuggerPath; - // debugger command: /usr/bin/gdb --interpreter=mi - string dbgCmdArguments = String.Format(CultureInfo.InvariantCulture, "{0} {1}", dbgPath, "--interpreter=mi"); + // debugger command: env DEBUGINFOD_TIMEOUT=30 /usr/bin/gdb --interpreter=mi + string dbgCmdArguments = String.Format(CultureInfo.InvariantCulture, "{0}{1} {2}", debuginfodPrefix, dbgPath, "--interpreter=mi"); string userArguments = ParseArguments(pipeArgs, quoteArgs); @@ -300,6 +319,12 @@ public EnvironmentEntry(Json.LaunchOptions.Environment jsonEntry) this.Value = jsonEntry.Value; } + public EnvironmentEntry(string name, string value) + { + this.Name = name; + this.Value = value; + } + /// /// [Required] Name of the environment variable /// @@ -805,6 +830,13 @@ public UnixShellPortLaunchOptions(string startRemoteDebuggerCommand, this.InitializeCommonOptions(baseLaunchOptions); this.BaseOptions = baseLaunchOptions; } + + // Prepend debuginfod env vars to the remote command. + string prefix = GetDebuginfodEnvironmentPrefix(); + if (!string.IsNullOrEmpty(prefix)) + { + this.StartRemoteDebuggerCommand = prefix + this.StartRemoteDebuggerCommand; + } } } @@ -1241,6 +1273,51 @@ public int DebuginfodTimeout } } + /// + /// Returns environment entries to configure debuginfod on the GDB process. + /// + public List GetDebuginfodEnvironmentEntries() + { + var entries = new List(); + if (DebuggerMIMode != MIMode.Gdb) + return entries; + + if (EnableDebuginfod) + { + if (DebuginfodTimeout > 0) + { + string timeoutStr = DebuginfodTimeout.ToString(System.Globalization.CultureInfo.InvariantCulture); + entries.Add(new EnvironmentEntry("DEBUGINFOD_TIMEOUT", timeoutStr)); + entries.Add(new EnvironmentEntry("DEBUGINFOD_MAXTIME", timeoutStr)); + } + } + else + { + entries.Add(new EnvironmentEntry("DEBUGINFOD_URLS", "")); + } + + return entries; + } + + /// + /// Returns an 'env' command prefix for debuginfod settings, for use in remote commands. + /// Returns empty string if no env vars are needed. + /// + public string GetDebuginfodEnvironmentPrefix() + { + var entries = GetDebuginfodEnvironmentEntries(); + if (entries.Count == 0) + return string.Empty; + + var sb = new StringBuilder(); + sb.Append("env "); + foreach (var entry in entries) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "{0}={1} ", entry.Name, entry.Value); + } + return sb.ToString(); + } + public string GetOptionsString() { try diff --git a/src/MICore/Transports/LocalTransport.cs b/src/MICore/Transports/LocalTransport.cs index 2fdecc932..85c3e56bd 100755 --- a/src/MICore/Transports/LocalTransport.cs +++ b/src/MICore/Transports/LocalTransport.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Text; using System.Threading; using System.Diagnostics; @@ -41,22 +40,9 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, proc.StartInfo.SetEnvironmentVariable("PATH", path); } - // Configure debuginfod environment variables on the GDB process. - if (options.DebuggerMIMode == MIMode.Gdb) + foreach (var entry in options.GetDebuginfodEnvironmentEntries()) { - if (options.EnableDebuginfod) - { - if (options.DebuginfodTimeout > 0) - { - string timeoutStr = options.DebuginfodTimeout.ToString(CultureInfo.InvariantCulture); - proc.StartInfo.SetEnvironmentVariable("DEBUGINFOD_TIMEOUT", timeoutStr); - proc.StartInfo.SetEnvironmentVariable("DEBUGINFOD_MAXTIME", timeoutStr); - } - } - else - { - proc.StartInfo.SetEnvironmentVariable("DEBUGINFOD_URLS", ""); - } + proc.StartInfo.SetEnvironmentVariable(entry.Name, entry.Value); } // Allow to execute custom commands before launching debugger. diff --git a/src/MICore/Transports/PipeTransport.cs b/src/MICore/Transports/PipeTransport.cs index c456ac590..94ec14562 100644 --- a/src/MICore/Transports/PipeTransport.cs +++ b/src/MICore/Transports/PipeTransport.cs @@ -144,6 +144,11 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, proc.StartInfo.SetEnvironmentVariable(entry.Name, entry.Value); } + foreach (var entry in options.GetDebuginfodEnvironmentEntries()) + { + proc.StartInfo.SetEnvironmentVariable(entry.Name, entry.Value); + } + InitProcess(proc, out reader, out writer); } diff --git a/src/MICore/Transports/RunInTerminalTransport.cs b/src/MICore/Transports/RunInTerminalTransport.cs index 4418f105d..a9fbb8fc5 100644 --- a/src/MICore/Transports/RunInTerminalTransport.cs +++ b/src/MICore/Transports/RunInTerminalTransport.cs @@ -163,7 +163,8 @@ public override async void Init(ITransportCallback transportCallback, LaunchOpti } // Do not pass the launchOptions Environment entries as those are used for the debuggee only. - RunInTerminalLauncher launcher = new RunInTerminalLauncher(windowtitle, new List(0).AsReadOnly()); + var debuggerEnv = options.GetDebuginfodEnvironmentEntries().AsReadOnly(); + RunInTerminalLauncher launcher = new RunInTerminalLauncher(windowtitle, debuggerEnv); launcher.Launch( cmdArgs, From 4b51451cab203513a84e839eb11612c0b236d19c Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Wed, 20 May 2026 11:55:11 -0700 Subject: [PATCH 3/4] Work only for "local" transports --- loc/lci/OpenFolderSchema.json.lci | 54 ++++++++++++++++++++++++ src/MICore/LaunchOptions.cs | 29 +++---------- src/MICore/Transports/PipeTransport.cs | 5 --- src/MIDebugPackage/OpenFolderSchema.json | 2 +- 4 files changed, 60 insertions(+), 30 deletions(-) diff --git a/loc/lci/OpenFolderSchema.json.lci b/loc/lci/OpenFolderSchema.json.lci index e3d3ac8b4..10049e79b 100644 --- a/loc/lci/OpenFolderSchema.json.lci +++ b/loc/lci/OpenFolderSchema.json.lci @@ -388,6 +388,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MICore/LaunchOptions.cs b/src/MICore/LaunchOptions.cs index f05bb7fe9..c53bc89d0 100644 --- a/src/MICore/LaunchOptions.cs +++ b/src/MICore/LaunchOptions.cs @@ -138,11 +138,10 @@ static internal PipeLaunchOptions CreateFromJson(JObject parsedOptions) } Json.LaunchOptions.BaseOptions baseOptions = Json.LaunchOptions.LaunchOptionHelpers.GetLaunchOrAttachOptions(parsedOptions); - string debuginfodPrefix = ComputeDebuginfodPrefixFromSettings(baseOptions.Debuginfod); PipeLaunchOptions pipeOptions = new PipeLaunchOptions( pipePath: pipeProgram, - pipeArguments: EnsurePipeArguments(pipeArgs, debuggerPath, gdbPathDefault, quoteArgs, debuginfodPrefix), + pipeArguments: EnsurePipeArguments(pipeArgs, debuggerPath, gdbPathDefault, quoteArgs), pipeCommandArguments: ParseArguments(pipeCmd??pipeArgs, quoteArgs), pipeCwd: pipeCwd, pipeEnvironment: GetEnvironmentEntries(pipeEnv) @@ -162,30 +161,13 @@ static internal PipeLaunchOptions CreateFromJson(JObject parsedOptions) return pipeOptions; } - private static string ComputeDebuginfodPrefixFromSettings(Json.LaunchOptions.DebuginfodSettings settings) - { - bool enabled = settings?.Enabled ?? true; - int timeout = settings?.Timeout ?? 30; - if (timeout < 0) timeout = 30; - - if (enabled && timeout > 0) - { - return string.Format(CultureInfo.InvariantCulture, "env DEBUGINFOD_TIMEOUT={0} DEBUGINFOD_MAXTIME={0} ", timeout); - } - else if (!enabled) - { - return "env DEBUGINFOD_URLS= "; - } - return string.Empty; - } - - private static string EnsurePipeArguments(List pipeArgs, string debuggerPath, string debuggerPathDefault, bool quoteArgs, string debuginfodPrefix = "") + private static string EnsurePipeArguments(List pipeArgs, string debuggerPath, string debuggerPathDefault, bool quoteArgs) { // Debugger path. Assume /usr/bin/gdb unless specified string dbgPath = String.IsNullOrWhiteSpace(debuggerPath) ? debuggerPathDefault : debuggerPath; - // debugger command: env DEBUGINFOD_TIMEOUT=30 /usr/bin/gdb --interpreter=mi - string dbgCmdArguments = String.Format(CultureInfo.InvariantCulture, "{0}{1} {2}", debuginfodPrefix, dbgPath, "--interpreter=mi"); + // debugger command: /usr/bin/gdb --interpreter=mi + string dbgCmdArguments = String.Format(CultureInfo.InvariantCulture, "{0} {1}", dbgPath, "--interpreter=mi"); string userArguments = ParseArguments(pipeArgs, quoteArgs); @@ -831,7 +813,6 @@ public UnixShellPortLaunchOptions(string startRemoteDebuggerCommand, this.BaseOptions = baseLaunchOptions; } - // Prepend debuginfod env vars to the remote command. string prefix = GetDebuginfodEnvironmentPrefix(); if (!string.IsNullOrEmpty(prefix)) { @@ -1300,7 +1281,7 @@ public List GetDebuginfodEnvironmentEntries() } /// - /// Returns an 'env' command prefix for debuginfod settings, for use in remote commands. + /// Returns an 'env' command prefix for debuginfod settings, for use in shell-based remote commands. /// Returns empty string if no env vars are needed. /// public string GetDebuginfodEnvironmentPrefix() diff --git a/src/MICore/Transports/PipeTransport.cs b/src/MICore/Transports/PipeTransport.cs index 94ec14562..c456ac590 100644 --- a/src/MICore/Transports/PipeTransport.cs +++ b/src/MICore/Transports/PipeTransport.cs @@ -144,11 +144,6 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, proc.StartInfo.SetEnvironmentVariable(entry.Name, entry.Value); } - foreach (var entry in options.GetDebuginfodEnvironmentEntries()) - { - proc.StartInfo.SetEnvironmentVariable(entry.Name, entry.Value); - } - InitProcess(proc, out reader, out writer); } diff --git a/src/MIDebugPackage/OpenFolderSchema.json b/src/MIDebugPackage/OpenFolderSchema.json index 44f473dc4..0039be008 100644 --- a/src/MIDebugPackage/OpenFolderSchema.json +++ b/src/MIDebugPackage/OpenFolderSchema.json @@ -206,7 +206,7 @@ }, "timeout": { "type": "integer", - "description": "The timeout in seconds for debuginfod server requests. Default is 30. Set to 0 for no timeout override (GDB defaults apply).", + "description": "The timeout in seconds for debuginfod server requests. Default is 30. Set to 0 for no timeout override (GDB defaults apply). Applies to local, runInTerminal, and SSH attach transports. This setting is not applied for pipeTransport; set DEBUGINFOD_TIMEOUT and DEBUGINFOD_MAXTIME in the debugger's environment manually.", "default": 30, "minimum": 0 } From 8c66aff3eedd1a9f829652bb72cdbe457e1f7417 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Wed, 20 May 2026 19:33:41 -0700 Subject: [PATCH 4/4] Update xsd --- src/MICore/LaunchOptions.xsd | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MICore/LaunchOptions.xsd b/src/MICore/LaunchOptions.xsd index cae1c94d8..5449b012e 100644 --- a/src/MICore/LaunchOptions.xsd +++ b/src/MICore/LaunchOptions.xsd @@ -221,6 +221,7 @@ The timeout in seconds for debuginfod server requests. Default is 30. Only applies when EnableDebuginfod is true. A value of 0 means no timeout override is applied (GDB/libdebuginfod defaults are used). This sets the DEBUGINFOD_TIMEOUT and DEBUGINFOD_MAXTIME environment variables on the GDB process. + Applies to local, runInTerminal, and SSH attach transports. This setting is not applied for pipeTransport; set DEBUGINFOD_TIMEOUT and DEBUGINFOD_MAXTIME in the debugger's environment manually.