[Lldb-commits] [lldb] [llvm] [lldb] Add HTTP support in SymbolLocatorSymStore (PR #186986)

Stefan Gränitz via lldb-commits lldb-commits at lists.llvm.org
Wed Mar 18 08:52:42 PDT 2026


https://github.com/weliveindetail updated https://github.com/llvm/llvm-project/pull/186986

>From 7c9d0094de26760c790b857d6c916953f96df6f4 Mon Sep 17 00:00:00 2001
From: Alexandre Ganea <aganea at havenstudios.com>
Date: Wed, 4 Mar 2026 10:10:59 +0100
Subject: [PATCH 1/7] Add WinHTTP-based client on Windows

---
 llvm/include/llvm/Support/HTTP/HTTPClient.h |   4 +-
 llvm/lib/Support/HTTP/CMakeLists.txt        |   5 +
 llvm/lib/Support/HTTP/HTTPClient.cpp        | 206 ++++++++++++++++++++
 3 files changed, 212 insertions(+), 3 deletions(-)

diff --git a/llvm/include/llvm/Support/HTTP/HTTPClient.h b/llvm/include/llvm/Support/HTTP/HTTPClient.h
index aa4727ad33024..594995505a40e 100644
--- a/llvm/include/llvm/Support/HTTP/HTTPClient.h
+++ b/llvm/include/llvm/Support/HTTP/HTTPClient.h
@@ -51,9 +51,7 @@ class HTTPResponseHandler {
 
 /// A reusable client that can perform HTTPRequests through a network socket.
 class HTTPClient {
-#ifdef LLVM_ENABLE_CURL
-  void *Curl = nullptr;
-#endif
+  void *Handle = nullptr;
 
 public:
   HTTPClient();
diff --git a/llvm/lib/Support/HTTP/CMakeLists.txt b/llvm/lib/Support/HTTP/CMakeLists.txt
index 9bf1da8c60c88..e7a0e6fe34110 100644
--- a/llvm/lib/Support/HTTP/CMakeLists.txt
+++ b/llvm/lib/Support/HTTP/CMakeLists.txt
@@ -8,6 +8,11 @@ if (LLVM_ENABLE_HTTPLIB)
   set(imported_libs ${imported_libs} httplib::httplib)
 endif()
 
+# Use WinHTTP on Windows
+if (WIN32)
+  set(imported_libs ${imported_libs} winhttp.lib)
+endif()
+
 add_llvm_component_library(LLVMSupportHTTP
   HTTPClient.cpp
   HTTPServer.cpp
diff --git a/llvm/lib/Support/HTTP/HTTPClient.cpp b/llvm/lib/Support/HTTP/HTTPClient.cpp
index 6301f86da4086..ec1961cb26658 100644
--- a/llvm/lib/Support/HTTP/HTTPClient.cpp
+++ b/llvm/lib/Support/HTTP/HTTPClient.cpp
@@ -140,6 +140,210 @@ unsigned HTTPClient::responseCode() {
 
 #else
 
+#ifdef _WIN32
+#include <windows.h>
+#include <winhttp.h>
+#pragma comment(lib, "winhttp.lib")
+
+namespace {
+
+struct WinHTTPSession {
+  HINTERNET SessionHandle = nullptr;
+  HINTERNET ConnectHandle = nullptr;
+  HINTERNET RequestHandle = nullptr;
+  DWORD ResponseCode = 0;
+
+  ~WinHTTPSession() {
+    if (RequestHandle)
+      WinHttpCloseHandle(RequestHandle);
+    if (ConnectHandle)
+      WinHttpCloseHandle(ConnectHandle);
+    if (SessionHandle)
+      WinHttpCloseHandle(SessionHandle);
+  }
+};
+
+bool convertUTF8ToWide(StringRef Utf8, std::wstring &Wide) {
+  int WideLen =
+      MultiByteToWideChar(CP_UTF8, 0, Utf8.data(), Utf8.size(), nullptr, 0);
+  if (WideLen <= 0)
+    return false;
+  Wide.resize(WideLen);
+  MultiByteToWideChar(CP_UTF8, 0, Utf8.data(), Utf8.size(), &Wide[0], WideLen);
+  return true;
+}
+
+bool parseURL(StringRef Url, std::wstring &Host, std::wstring &Path,
+              INTERNET_PORT &Port, bool &Secure) {
+  // Parse URL: http://host:port/path
+  if (Url.starts_with("https://")) {
+    Secure = true;
+    Url = Url.drop_front(8);
+  } else if (Url.starts_with("http://")) {
+    Secure = false;
+    Url = Url.drop_front(7);
+  } else {
+    return false;
+  }
+
+  size_t SlashPos = Url.find('/');
+  StringRef HostPort =
+      (SlashPos != StringRef::npos) ? Url.substr(0, SlashPos) : Url;
+  StringRef PathPart =
+      (SlashPos != StringRef::npos) ? Url.substr(SlashPos) : StringRef("/");
+
+  size_t ColonPos = HostPort.find(':');
+  StringRef HostStr =
+      (ColonPos != StringRef::npos) ? HostPort.substr(0, ColonPos) : HostPort;
+
+  if (!convertUTF8ToWide(HostStr, Host))
+    return false;
+  if (!convertUTF8ToWide(PathPart, Path))
+    return false;
+
+  if (ColonPos != StringRef::npos) {
+    StringRef PortStr = HostPort.substr(ColonPos + 1);
+    Port = static_cast<INTERNET_PORT>(std::stoi(PortStr.str()));
+  } else {
+    Port = Secure ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT;
+  }
+
+  return true;
+}
+
+} // namespace
+
+HTTPClient::HTTPClient() : Handle(new WinHTTPSession()) {}
+
+HTTPClient::~HTTPClient() { delete static_cast<WinHTTPSession *>(Handle); }
+
+bool HTTPClient::isAvailable() { return true; }
+
+void HTTPClient::initialize() {
+  if (!IsInitialized) {
+    IsInitialized = true;
+  }
+}
+
+void HTTPClient::cleanup() {
+  if (IsInitialized) {
+    IsInitialized = false;
+  }
+}
+
+void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {
+  WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
+  if (Session && Session->SessionHandle) {
+    DWORD TimeoutMs = static_cast<DWORD>(Timeout.count());
+    WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_CONNECT_TIMEOUT,
+                     &TimeoutMs, sizeof(TimeoutMs));
+    WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_RECEIVE_TIMEOUT,
+                     &TimeoutMs, sizeof(TimeoutMs));
+    WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_SEND_TIMEOUT,
+                     &TimeoutMs, sizeof(TimeoutMs));
+  }
+}
+
+Error HTTPClient::perform(const HTTPRequest &Request,
+                          HTTPResponseHandler &Handler) {
+  if (Request.Method != HTTPMethod::GET)
+    return createStringError(errc::invalid_argument,
+                             "Only GET requests are supported.");
+
+  WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
+
+  // Parse URL
+  std::wstring Host, Path;
+  INTERNET_PORT Port = 0;
+  bool Secure = false;
+  if (!parseURL(Request.Url, Host, Path, Port, Secure))
+    return createStringError(errc::invalid_argument,
+                             "Invalid URL: " + Request.Url);
+
+  // Create session
+  Session->SessionHandle =
+      WinHttpOpen(L"LLVM-HTTPClient/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
+                  WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
+  if (!Session->SessionHandle)
+    return createStringError(errc::io_error, "Failed to open WinHTTP session");
+
+  // Create connection
+  Session->ConnectHandle =
+      WinHttpConnect(Session->SessionHandle, Host.c_str(), Port, 0);
+  if (!Session->ConnectHandle) {
+    return createStringError(errc::io_error,
+                             "Failed to connect to host: " + Request.Url);
+  }
+
+  // Open request
+  DWORD Flags = WINHTTP_FLAG_REFRESH;
+  if (Secure)
+    Flags |= WINHTTP_FLAG_SECURE;
+
+  Session->RequestHandle = WinHttpOpenRequest(
+      Session->ConnectHandle, L"GET", Path.c_str(), nullptr, WINHTTP_NO_REFERER,
+      WINHTTP_DEFAULT_ACCEPT_TYPES, Flags);
+  if (!Session->RequestHandle)
+    return createStringError(errc::io_error, "Failed to open HTTP request");
+
+  // Add headers
+  for (const std::string &Header : Request.Headers) {
+    std::wstring WideHeader;
+    if (!convertUTF8ToWide(Header, WideHeader))
+      continue;
+    WinHttpAddRequestHeaders(Session->RequestHandle, WideHeader.c_str(),
+                             static_cast<DWORD>(WideHeader.length()),
+                             WINHTTP_ADDREQ_FLAG_ADD);
+  }
+
+  // Send request
+  if (!WinHttpSendRequest(Session->RequestHandle, WINHTTP_NO_ADDITIONAL_HEADERS,
+                          0, nullptr, 0, 0, 0))
+    return createStringError(errc::io_error, "Failed to send HTTP request");
+
+  // Receive response
+  if (!WinHttpReceiveResponse(Session->RequestHandle, nullptr))
+    return createStringError(errc::io_error, "Failed to receive HTTP response");
+
+  // Get response code
+  DWORD CodeSize = sizeof(Session->ResponseCode);
+  if (!WinHttpQueryHeaders(Session->RequestHandle,
+                           WINHTTP_QUERY_STATUS_CODE |
+                               WINHTTP_QUERY_FLAG_NUMBER,
+                           WINHTTP_HEADER_NAME_BY_INDEX, &Session->ResponseCode,
+                           &CodeSize, nullptr))
+    Session->ResponseCode = 0;
+
+  // Read response body
+  DWORD BytesAvailable = 0;
+  while (WinHttpQueryDataAvailable(Session->RequestHandle, &BytesAvailable)) {
+    if (BytesAvailable == 0)
+      break;
+
+    std::vector<char> Buffer(BytesAvailable);
+    DWORD BytesRead = 0;
+    if (!WinHttpReadData(Session->RequestHandle, Buffer.data(), BytesAvailable,
+                         &BytesRead))
+      return createStringError(errc::io_error, "Failed to read HTTP response");
+
+    if (BytesRead > 0) {
+      if (Error Err =
+              Handler.handleBodyChunk(StringRef(Buffer.data(), BytesRead)))
+        return Err;
+    }
+  }
+
+  return Error::success();
+}
+
+unsigned HTTPClient::responseCode() {
+  WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
+  return Session ? Session->ResponseCode : 0;
+}
+
+#else // _WIN32
+
+// Non-Windows, non-libcurl stub implementations
 HTTPClient::HTTPClient() = default;
 
 HTTPClient::~HTTPClient() = default;
@@ -161,4 +365,6 @@ unsigned HTTPClient::responseCode() {
   llvm_unreachable("No HTTP Client implementation available.");
 }
 
+#endif // _WIN32
+
 #endif

>From 6d7d6f1710afb8d1a614f8e2ce08bcb3457d7250 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Wed, 4 Mar 2026 13:27:52 +0100
Subject: [PATCH 2/7] Apply variable renaming in CURL based implementation

---
 llvm/lib/Support/HTTP/HTTPClient.cpp | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/llvm/lib/Support/HTTP/HTTPClient.cpp b/llvm/lib/Support/HTTP/HTTPClient.cpp
index ec1961cb26658..43d688cddecec 100644
--- a/llvm/lib/Support/HTTP/HTTPClient.cpp
+++ b/llvm/lib/Support/HTTP/HTTPClient.cpp
@@ -64,7 +64,7 @@ void HTTPClient::cleanup() {
 void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {
   if (Timeout < std::chrono::milliseconds(0))
     Timeout = std::chrono::milliseconds(0);
-  curl_easy_setopt(Curl, CURLOPT_TIMEOUT_MS, Timeout.count());
+  curl_easy_setopt(Handle, CURLOPT_TIMEOUT_MS, Timeout.count());
 }
 
 /// CurlHTTPRequest and the curl{Header,Write}Function are implementation
@@ -93,17 +93,17 @@ static size_t curlWriteFunction(char *Contents, size_t Size, size_t NMemb,
 HTTPClient::HTTPClient() {
   assert(IsInitialized &&
          "Must call HTTPClient::initialize() at the beginning of main().");
-  if (Curl)
+  if (Handle)
     return;
-  Curl = curl_easy_init();
-  assert(Curl && "Curl could not be initialized");
+  Handle = curl_easy_init();
+  assert(Handle && "Curl could not be initialized");
   // Set the callback hooks.
-  curl_easy_setopt(Curl, CURLOPT_WRITEFUNCTION, curlWriteFunction);
+  curl_easy_setopt(Handle, CURLOPT_WRITEFUNCTION, curlWriteFunction);
   // Detect supported compressed encodings and accept all.
-  curl_easy_setopt(Curl, CURLOPT_ACCEPT_ENCODING, "");
+  curl_easy_setopt(Handle, CURLOPT_ACCEPT_ENCODING, "");
 }
 
-HTTPClient::~HTTPClient() { curl_easy_cleanup(Curl); }
+HTTPClient::~HTTPClient() { curl_easy_cleanup(Handle); }
 
 Error HTTPClient::perform(const HTTPRequest &Request,
                           HTTPResponseHandler &Handler) {
@@ -112,17 +112,17 @@ Error HTTPClient::perform(const HTTPRequest &Request,
                              "Unsupported CURL request method.");
 
   SmallString<128> Url = Request.Url;
-  curl_easy_setopt(Curl, CURLOPT_URL, Url.c_str());
-  curl_easy_setopt(Curl, CURLOPT_FOLLOWLOCATION, Request.FollowRedirects);
+  curl_easy_setopt(Handle, CURLOPT_URL, Url.c_str());
+  curl_easy_setopt(Handle, CURLOPT_FOLLOWLOCATION, Request.FollowRedirects);
 
   curl_slist *Headers = nullptr;
   for (const std::string &Header : Request.Headers)
     Headers = curl_slist_append(Headers, Header.c_str());
-  curl_easy_setopt(Curl, CURLOPT_HTTPHEADER, Headers);
+  curl_easy_setopt(Handle, CURLOPT_HTTPHEADER, Headers);
 
   CurlHTTPRequest CurlRequest(Handler);
-  curl_easy_setopt(Curl, CURLOPT_WRITEDATA, &CurlRequest);
-  CURLcode CurlRes = curl_easy_perform(Curl);
+  curl_easy_setopt(Handle, CURLOPT_WRITEDATA, &CurlRequest);
+  CURLcode CurlRes = curl_easy_perform(Handle);
   curl_slist_free_all(Headers);
   if (CurlRes != CURLE_OK)
     return joinErrors(std::move(CurlRequest.ErrorState),
@@ -134,7 +134,7 @@ Error HTTPClient::perform(const HTTPRequest &Request,
 
 unsigned HTTPClient::responseCode() {
   long Code = 0;
-  curl_easy_getinfo(Curl, CURLINFO_RESPONSE_CODE, &Code);
+  curl_easy_getinfo(Handle, CURLINFO_RESPONSE_CODE, &Code);
   return Code;
 }
 

>From 595b712cd346d909e63e0154649be8864f5be625 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Wed, 4 Mar 2026 13:30:30 +0100
Subject: [PATCH 3/7] Cover WinHTTP based implementation in new test variant of
 llvm-debuginfod-find

---
 .../{headers.test => headers-curl.test}       |  0
 .../llvm-debuginfod-find/headers-winhttp.test | 31 +++++++++++++++++++
 2 files changed, 31 insertions(+)
 rename llvm/test/tools/llvm-debuginfod-find/{headers.test => headers-curl.test} (100%)
 create mode 100644 llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test

diff --git a/llvm/test/tools/llvm-debuginfod-find/headers.test b/llvm/test/tools/llvm-debuginfod-find/headers-curl.test
similarity index 100%
rename from llvm/test/tools/llvm-debuginfod-find/headers.test
rename to llvm/test/tools/llvm-debuginfod-find/headers-curl.test
diff --git a/llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test b/llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test
new file mode 100644
index 0000000000000..96a73c6bf373a
--- /dev/null
+++ b/llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test
@@ -0,0 +1,31 @@
+REQUIRES: system-windows
+
+RUN: rm -rf %t
+RUN: mkdir -p %t/debuginfod-cache
+RUN: %python %S/Inputs/capture_req.py llvm-debuginfod-find --debuginfo 0 \
+RUN:   | FileCheck --check-prefix NO-HEADERS %s
+RUN: env DEBUGINFOD_CACHE=%t/debuginfod-cache DEBUGINFOD_HEADERS_FILE=bad %python %S/Inputs/capture_req.py \
+RUN:   llvm-debuginfod-find --debuginfo 0 \
+RUN:   | FileCheck --check-prefix NO-HEADERS %s
+RUN: rm -rf %t/debuginfod-cache/*
+RUN: env DEBUGINFOD_CACHE=%t/debuginfod-cache DEBUGINFOD_HEADERS_FILE=%S/Inputs/headers %python %S/Inputs/capture_req.py \
+RUN:   llvm-debuginfod-find --debuginfo 0 \
+RUN:   | FileCheck --check-prefix HEADERS %s
+RUN: rm -rf %t/debuginfod-cache/*
+RUN: env DEBUGINFOD_CACHE=%t/debuginfod-cache DEBUGINFOD_HEADERS_FILE=%S/Inputs/headers DEBUGINFOD_URLS=fake not llvm-debuginfod-find --debuginfo 0 2>&1 \
+RUN:   | FileCheck --check-prefix ERR -DHEADER_FILE=%S/Inputs/headers %s
+
+NO-HEADERS:      User-Agent: LLVM-HTTPClient/1.0
+NO-HEADERS-NEXT: Host: localhost:{{[0-9]+}}
+
+HEADERS:      User-Agent: LLVM-HTTPClient/1.0
+HEADERS-NEXT: A: B
+HEADERS-NEXT: C: D
+HEADERS-NEXT: E: F
+HEADERS-NEXT: hi!$:		j			k
+HEADERS-NEXT: Host: localhost:{{[0-9]+}}
+
+ERR:      warning: could not parse debuginfod header: [[HEADER_FILE]]:3
+ERR-NEXT: warning: could not parse debuginfod header: [[HEADER_FILE]]:4
+ERR-NEXT: warning: could not parse debuginfod header: [[HEADER_FILE]]:5
+ERR-NEXT: warning: could not parse debuginfod header: [[HEADER_FILE]]:6

>From 41d6a45e72735f0d6ea6d530f063c7f87a9ca2de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Thu, 12 Mar 2026 17:16:49 +0100
Subject: [PATCH 4/7] [lldb] Add HTTP support in SymbolLocatorSymStore

---
 .../SymbolLocator/SymStore/CMakeLists.txt     |   1 +
 .../SymStore/SymbolLocatorSymStore.cpp        | 132 ++++++++++++++++--
 .../{TestSymStoreLocal.py => TestSymStore.py} |  42 +++++-
 3 files changed, 162 insertions(+), 13 deletions(-)
 rename lldb/test/API/symstore/{TestSymStoreLocal.py => TestSymStore.py} (75%)

diff --git a/lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt b/lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt
index b0da27f26c6a8..775e0284cd8af 100644
--- a/lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt
+++ b/lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt
@@ -13,6 +13,7 @@ add_lldb_library(lldbPluginSymbolLocatorSymStore PLUGIN
     lldbCore
     lldbHost
     lldbSymbol
+    LLVMSupportHTTP
   )
 
 add_dependencies(lldbPluginSymbolLocatorSymStore
diff --git a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
index d008a7d3e8e9a..7663120984239 100644
--- a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
+++ b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
@@ -20,6 +20,8 @@
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/Support/Endian.h"
 #include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/HTTP/HTTPClient.h"
 #include "llvm/Support/Path.h"
 
 using namespace lldb;
@@ -65,11 +67,15 @@ static PluginProperties &GetGlobalPluginProperties() {
 SymbolLocatorSymStore::SymbolLocatorSymStore() : SymbolLocator() {}
 
 void SymbolLocatorSymStore::Initialize() {
-  // First version can only locate PDB in local SymStore (no download yet).
-  PluginManager::RegisterPlugin(
-      GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
-      nullptr, LocateExecutableSymbolFile, nullptr, nullptr,
-      SymbolLocatorSymStore::DebuggerInitialize);
+  static llvm::once_flag g_once_flag;
+
+  llvm::call_once(g_once_flag, []() {
+    PluginManager::RegisterPlugin(
+        GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
+        nullptr, LocateExecutableSymbolFile, nullptr, nullptr,
+        SymbolLocatorSymStore::DebuggerInitialize);
+    llvm::HTTPClient::initialize();
+  });
 }
 
 void SymbolLocatorSymStore::DebuggerInitialize(Debugger &debugger) {
@@ -85,6 +91,7 @@ void SymbolLocatorSymStore::DebuggerInitialize(Debugger &debugger) {
 
 void SymbolLocatorSymStore::Terminate() {
   PluginManager::UnregisterPlugin(CreateInstance);
+  llvm::HTTPClient::cleanup();
 }
 
 llvm::StringRef SymbolLocatorSymStore::GetPluginDescriptionStatic() {
@@ -95,6 +102,8 @@ SymbolLocator *SymbolLocatorSymStore::CreateInstance() {
   return new SymbolLocatorSymStore();
 }
 
+namespace {
+
 // RSDS entries store identity as a 20-byte UUID composed of 16-byte GUID and
 // 4-byte age:
 //   12345678-1234-5678-9ABC-DEF012345678-00000001
@@ -102,13 +111,115 @@ SymbolLocator *SymbolLocatorSymStore::CreateInstance() {
 // SymStore key is a string with no separators and age as decimal:
 //   12345678123456789ABCDEF0123456781
 //
-static std::string formatSymStoreKey(const UUID &uuid) {
+std::string formatSymStoreKey(const UUID &uuid) {
   llvm::ArrayRef<uint8_t> bytes = uuid.GetBytes();
   uint32_t age = llvm::support::endian::read32be(bytes.data() + 16);
   constexpr bool LowerCase = false;
   return llvm::toHex(bytes.slice(0, 16), LowerCase) + std::to_string(age);
 }
 
+// This is a simple version of Debuginfod's StreamedHTTPResponseHandler. We
+// should consider reusing that once we introduce caching.
+class FileDownloadHandler : public llvm::HTTPResponseHandler {
+private:
+  std::error_code m_ec;
+  llvm::raw_fd_ostream m_stream;
+
+public:
+  FileDownloadHandler(llvm::StringRef file) : m_stream(file.str(), m_ec) {}
+  virtual ~FileDownloadHandler() = default;
+
+  llvm::Error handleBodyChunk(llvm::StringRef data) override {
+    // Propagate error from ctor
+    if (m_ec)
+      return llvm::createStringError(m_ec, "Failed to open file for writing");
+    m_stream.write(data.data(), data.size());
+    if (std::error_code ec = m_stream.error())
+      return llvm::createStringError(ec, "Error writing to file");
+
+    return llvm::Error::success();
+  }
+};
+
+llvm::Error downloadFileHTTP(llvm::StringRef url, FileDownloadHandler dest) {
+  if (!llvm::HTTPClient::isAvailable())
+    return llvm::createStringError(
+        std::make_error_code(std::errc::not_supported),
+        "HTTP client is not available");
+  llvm::HTTPRequest Request(url);
+  Request.FollowRedirects = true;
+
+  llvm::HTTPClient Client;
+  Client.setTimeout(std::chrono::seconds(60));
+
+  if (llvm::Error Err = Client.perform(Request, dest))
+    return Err;
+
+  unsigned ResponseCode = Client.responseCode();
+  if (ResponseCode != 200) {
+    return llvm::createStringError(std::make_error_code(std::errc::io_error),
+                                   "HTTP request failed with status code " +
+                                       std::to_string(ResponseCode));
+  }
+
+  return llvm::Error::success();
+}
+
+std::optional<FileSpec>
+requestFileFromSymStoreServerHTTP(llvm::StringRef base_url, llvm::StringRef key,
+                                  llvm::StringRef pdb_name) {
+  using namespace llvm::sys;
+  Log *log = GetLog(LLDBLog::Symbols);
+
+  // Construct the path for local storage. Configurable cache coming soon.
+  llvm::SmallString<128> cache_file;
+  if (!path::cache_directory(cache_file)) {
+    LLDB_LOGV(log, "Failed to determine cache directory for SymStore");
+    return {};
+  }
+  path::append(cache_file, "lldb", "SymStore", pdb_name, key);
+  if (std::error_code ec = fs::create_directories(cache_file)) {
+    LLDB_LOG(log, "Failed to create temporary folder '{0}': {1}", cache_file,
+             ec.message());
+    return {};
+  }
+  path::append(cache_file, pdb_name);
+
+  // Server has same directory structure with forward slashes as separators.
+  std::string source_url =
+      llvm::formatv("{0}/{1}/{2}/{1}", base_url, pdb_name, key);
+  if (llvm::Error err = downloadFileHTTP(source_url, cache_file.str())) {
+    LLDB_LOG_ERROR(log, std::move(err),
+                   "  Failed to download from SymStore '{1}': {0}", source_url);
+    return {};
+  }
+
+  return FileSpec(cache_file.str());
+}
+
+std::optional<FileSpec> findFileInLocalSymStore(llvm::StringRef root_dir,
+                                                llvm::StringRef key,
+                                                llvm::StringRef pdb_name) {
+  llvm::SmallString<256> path;
+  llvm::sys::path::append(path, root_dir, pdb_name, key, pdb_name);
+  FileSpec spec(path);
+  if (!FileSystem::Instance().Exists(spec))
+    return {};
+
+  return spec;
+}
+
+std::optional<FileSpec> locateSymStoreEntry(llvm::StringRef base_url,
+                                            llvm::StringRef key,
+                                            llvm::StringRef pdb_name) {
+  if (base_url.starts_with("http://") || base_url.starts_with("https://"))
+    return requestFileFromSymStoreServerHTTP(base_url, key, pdb_name);
+
+  return findFileInLocalSymStore(base_url, key, pdb_name);
+}
+
+} // namespace
+
 std::optional<FileSpec> SymbolLocatorSymStore::LocateExecutableSymbolFile(
     const ModuleSpec &module_spec, const FileSpecList &default_search_paths) {
   const UUID &uuid = module_spec.GetUUID();
@@ -134,12 +245,9 @@ std::optional<FileSpec> SymbolLocatorSymStore::LocateExecutableSymbolFile(
   std::string key = formatSymStoreKey(uuid);
   Args sym_store_urls = GetGlobalPluginProperties().GetURLs();
   for (const Args::ArgEntry &url : sym_store_urls) {
-    llvm::SmallString<256> path;
-    llvm::sys::path::append(path, url.ref(), pdb_name, key, pdb_name);
-    FileSpec spec(path);
-    if (FileSystem::Instance().Exists(spec)) {
-      LLDB_LOGV(log, "Found {0} in SymStore {1}", pdb_name, url.ref());
-      return spec;
+    if (auto spec = locateSymStoreEntry(url.ref(), key, pdb_name)) {
+      LLDB_LOGV(log, "  Found {0} in SymStore {1}", pdb_name, url.ref());
+      return *spec;
     }
   }
 
diff --git a/lldb/test/API/symstore/TestSymStoreLocal.py b/lldb/test/API/symstore/TestSymStore.py
similarity index 75%
rename from lldb/test/API/symstore/TestSymStoreLocal.py
rename to lldb/test/API/symstore/TestSymStore.py
index 98569d2b8c66f..4fcac80ee546a 100644
--- a/lldb/test/API/symstore/TestSymStoreLocal.py
+++ b/lldb/test/API/symstore/TestSymStore.py
@@ -1,5 +1,9 @@
+import http.server
 import os
 import shutil
+import socketserver
+import threading
+from functools import partial
 
 import lldb
 from lldbsuite.test.decorators import *
@@ -62,7 +66,31 @@ def __exit__(self, *exc_info):
         self._test.runCmd("settings clear plugin.symbol-locator.symstore")
 
 
-class SymStoreLocalTests(TestBase):
+class HTTPServer:
+    """
+    Context Manager to serve a local directory tree via HTTP.
+    """
+
+    def __init__(self, dir):
+        address = ("localhost", 0)  # auto-select free port
+        handler = partial(http.server.SimpleHTTPRequestHandler, directory=dir)
+        self._server = socketserver.ThreadingTCPServer(address, handler)
+        self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
+
+    def __enter__(self):
+        self._thread.start()
+        host, port = self._server.server_address
+        return f"http://{host}:{port}"
+
+    def __exit__(self, *exc_info):
+        if self._server:
+            self._server.shutdown()
+            self._server.server_close()
+        if self._thread:
+            self._thread.join()
+
+
+class SymStoreTests(TestBase):
     SHARED_BUILD_TESTCASE = False
     TEST_WITH_PDB_DEBUG_INFO = True
 
@@ -115,3 +143,15 @@ def test_local_dir(self):
                 f"settings set plugin.symbol-locator.symstore.urls {symstore_dir}"
             )
             self.try_breakpoint(exe, should_have_loc=True)
+
+    def test_http(self):
+        """
+        Check that breakpoint hits with remote SymStore.
+        """
+        exe, sym = self.build_inferior()
+        with MockedSymStore(self, exe, sym) as symstore_dir:
+            with HTTPServer(symstore_dir) as url:
+                self.runCmd(
+                    f"settings set plugin.symbol-locator.symstore.urls {url}"
+                )
+                self.try_breakpoint(exe, should_have_loc=True)

>From 091e91263aeacb75852d7932858779ce1b8fea14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Wed, 18 Mar 2026 13:18:07 +0100
Subject: [PATCH 5/7] Address feedback

---
 .../SymStore/SymbolLocatorSymStore.cpp        | 31 +++++++++++
 llvm/include/llvm/Support/HTTP/HTTPClient.h   |  2 +
 llvm/lib/Support/HTTP/HTTPClient.cpp          | 53 ++++++++++++++-----
 3 files changed, 73 insertions(+), 13 deletions(-)

diff --git a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
index 7663120984239..7df6198c837c4 100644
--- a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
+++ b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
@@ -150,6 +150,9 @@ llvm::Error downloadFileHTTP(llvm::StringRef url, FileDownloadHandler dest) {
   Request.FollowRedirects = true;
 
   llvm::HTTPClient Client;
+
+  // TODO: Since PDBs can be huge, we should distinguish between resolve,
+  // connect, send and receive.
   Client.setTimeout(std::chrono::seconds(60));
 
   if (llvm::Error Err = Client.perform(Request, dest))
@@ -165,12 +168,37 @@ llvm::Error downloadFileHTTP(llvm::StringRef url, FileDownloadHandler dest) {
   return llvm::Error::success();
 }
 
+bool has_unsafe_characters(llvm::StringRef s) {
+  for (unsigned char c : s) {
+    // RFC 3986 unreserved characters are safe for file names and URLs.
+    if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
+        (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' ||
+        c == '~') {
+      continue;
+    }
+
+    return true;
+  }
+
+  // Avoid path semantics issues.
+  return s == "." || s == "..";
+}
+
 std::optional<FileSpec>
 requestFileFromSymStoreServerHTTP(llvm::StringRef base_url, llvm::StringRef key,
                                   llvm::StringRef pdb_name) {
   using namespace llvm::sys;
   Log *log = GetLog(LLDBLog::Symbols);
 
+  //// Make sure URL will be valid, portable, and compatible with symbol servers
+  if (has_unsafe_characters(pdb_name)) {
+    LLDB_LOGV(log,
+              "Rejecting HTTP lookup for PDB file due to unsafe characters in "
+              "name: {0}",
+              pdb_name);
+    return {};
+  }
+
   // Construct the path for local storage. Configurable cache coming soon.
   llvm::SmallString<128> cache_file;
   if (!path::cache_directory(cache_file)) {
@@ -215,6 +243,9 @@ std::optional<FileSpec> locateSymStoreEntry(llvm::StringRef base_url,
   if (base_url.starts_with("http://") || base_url.starts_with("https://"))
     return requestFileFromSymStoreServerHTTP(base_url, key, pdb_name);
 
+  if (base_url.starts_with("file://"))
+    base_url = base_url.drop_front(7);
+
   return findFileInLocalSymStore(base_url, key, pdb_name);
 }
 
diff --git a/llvm/include/llvm/Support/HTTP/HTTPClient.h b/llvm/include/llvm/Support/HTTP/HTTPClient.h
index 594995505a40e..17b706ff3e083 100644
--- a/llvm/include/llvm/Support/HTTP/HTTPClient.h
+++ b/llvm/include/llvm/Support/HTTP/HTTPClient.h
@@ -51,7 +51,9 @@ class HTTPResponseHandler {
 
 /// A reusable client that can perform HTTPRequests through a network socket.
 class HTTPClient {
+#if defined(LLVM_ENABLE_CURL) || defined(_WIN32)
   void *Handle = nullptr;
+#endif
 
 public:
   HTTPClient();
diff --git a/llvm/lib/Support/HTTP/HTTPClient.cpp b/llvm/lib/Support/HTTP/HTTPClient.cpp
index 43d688cddecec..69780b32d1cf0 100644
--- a/llvm/lib/Support/HTTP/HTTPClient.cpp
+++ b/llvm/lib/Support/HTTP/HTTPClient.cpp
@@ -23,6 +23,9 @@
 #ifdef LLVM_ENABLE_CURL
 #include <curl/curl.h>
 #endif
+#ifdef _WIN32
+#include "llvm/Support/ConvertUTF.h"
+#endif
 
 using namespace llvm;
 
@@ -163,16 +166,6 @@ struct WinHTTPSession {
   }
 };
 
-bool convertUTF8ToWide(StringRef Utf8, std::wstring &Wide) {
-  int WideLen =
-      MultiByteToWideChar(CP_UTF8, 0, Utf8.data(), Utf8.size(), nullptr, 0);
-  if (WideLen <= 0)
-    return false;
-  Wide.resize(WideLen);
-  MultiByteToWideChar(CP_UTF8, 0, Utf8.data(), Utf8.size(), &Wide[0], WideLen);
-  return true;
-}
-
 bool parseURL(StringRef Url, std::wstring &Host, std::wstring &Path,
               INTERNET_PORT &Port, bool &Secure) {
   // Parse URL: http://host:port/path
@@ -196,9 +189,9 @@ bool parseURL(StringRef Url, std::wstring &Host, std::wstring &Path,
   StringRef HostStr =
       (ColonPos != StringRef::npos) ? HostPort.substr(0, ColonPos) : HostPort;
 
-  if (!convertUTF8ToWide(HostStr, Host))
+  if (!llvm::ConvertUTF8toWide(HostStr, Host))
     return false;
-  if (!convertUTF8ToWide(PathPart, Path))
+  if (!llvm::ConvertUTF8toWide(PathPart, Path))
     return false;
 
   if (ColonPos != StringRef::npos) {
@@ -249,6 +242,12 @@ Error HTTPClient::perform(const HTTPRequest &Request,
   if (Request.Method != HTTPMethod::GET)
     return createStringError(errc::invalid_argument,
                              "Only GET requests are supported.");
+  for (const std::string &Header : Request.Headers)
+    if (Header.find("\r") != std::string::npos ||
+        Header.find("\n") != std::string::npos) {
+      return createStringError(errc::invalid_argument,
+                               "Unsafe request can lead to header injection.");
+    }
 
   WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
 
@@ -267,6 +266,18 @@ Error HTTPClient::perform(const HTTPRequest &Request,
   if (!Session->SessionHandle)
     return createStringError(errc::io_error, "Failed to open WinHTTP session");
 
+  // Prevent fallback to TLS 1.0/1.1
+  DWORD SecureProtocols =
+      WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;
+  if (!WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_SECURE_PROTOCOLS,
+                        &SecureProtocols, sizeof(SecureProtocols)))
+    return createStringError(errc::io_error, "Failed to set secure protocols");
+
+  // Use HTTP/2 if available
+  DWORD EnableHttp2 = WINHTTP_PROTOCOL_FLAG_HTTP2;
+  WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL,
+                   &EnableHttp2, sizeof(EnableHttp2));
+
   // Create connection
   Session->ConnectHandle =
       WinHttpConnect(Session->SessionHandle, Host.c_str(), Port, 0);
@@ -286,10 +297,26 @@ Error HTTPClient::perform(const HTTPRequest &Request,
   if (!Session->RequestHandle)
     return createStringError(errc::io_error, "Failed to open HTTP request");
 
+  // Enforce checks that certificate wasn't revoked.
+  DWORD EnableRevocationChecks = WINHTTP_ENABLE_SSL_REVOCATION;
+  if (!WinHttpSetOption(Session->RequestHandle, WINHTTP_OPTION_ENABLE_FEATURE,
+                        &EnableRevocationChecks,
+                        sizeof(EnableRevocationChecks)))
+    return createStringError(errc::io_error,
+                             "Failed to enable certificate revocation checks");
+
+  // Explicitly enforce default validation. This protects against insecure
+  // overrides like SECURITY_FLAG_IGNORE_UNKNOWN_CA.
+  DWORD SecurityFlags = 0;
+  if (!WinHttpSetOption(Session->RequestHandle, WINHTTP_OPTION_SECURITY_FLAGS,
+                        &SecurityFlags, sizeof(SecurityFlags)))
+    return createStringError(errc::io_error,
+                             "Failed to enforce security flags");
+
   // Add headers
   for (const std::string &Header : Request.Headers) {
     std::wstring WideHeader;
-    if (!convertUTF8ToWide(Header, WideHeader))
+    if (!llvm::ConvertUTF8toWide(Header, WideHeader))
       continue;
     WinHttpAddRequestHeaders(Session->RequestHandle, WideHeader.c_str(),
                              static_cast<DWORD>(WideHeader.length()),

>From 56093ee2ce28d599da96e4c0c2f0c23d21d11e48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Wed, 18 Mar 2026 13:26:23 +0100
Subject: [PATCH 6/7] Format test code (NFC)

---
 lldb/test/API/symstore/TestSymStore.py | 20 +++++++-------------
 1 file changed, 7 insertions(+), 13 deletions(-)

diff --git a/lldb/test/API/symstore/TestSymStore.py b/lldb/test/API/symstore/TestSymStore.py
index 4fcac80ee546a..60b29bb19f2c6 100644
--- a/lldb/test/API/symstore/TestSymStore.py
+++ b/lldb/test/API/symstore/TestSymStore.py
@@ -127,10 +127,8 @@ def test_external_lookup_off(self):
         Check that breakpoint doesn't resolve with external lookup disabled.
         """
         exe, sym = self.build_inferior()
-        with MockedSymStore(self, exe, sym) as symstore_dir:
-            self.runCmd(
-                f"settings set plugin.symbol-locator.symstore.urls {symstore_dir}"
-            )
+        with MockedSymStore(self, exe, sym) as dir:
+            self.runCmd(f"settings set plugin.symbol-locator.symstore.urls {dir}")
             self.try_breakpoint(exe, ext_lookup=False, should_have_loc=False)
 
     def test_local_dir(self):
@@ -138,10 +136,8 @@ def test_local_dir(self):
         Check that breakpoint resolves with local SymStore.
         """
         exe, sym = self.build_inferior()
-        with MockedSymStore(self, exe, sym) as symstore_dir:
-            self.runCmd(
-                f"settings set plugin.symbol-locator.symstore.urls {symstore_dir}"
-            )
+        with MockedSymStore(self, exe, sym) as dir:
+            self.runCmd(f"settings set plugin.symbol-locator.symstore.urls {dir}")
             self.try_breakpoint(exe, should_have_loc=True)
 
     def test_http(self):
@@ -149,9 +145,7 @@ def test_http(self):
         Check that breakpoint hits with remote SymStore.
         """
         exe, sym = self.build_inferior()
-        with MockedSymStore(self, exe, sym) as symstore_dir:
-            with HTTPServer(symstore_dir) as url:
-                self.runCmd(
-                    f"settings set plugin.symbol-locator.symstore.urls {url}"
-                )
+        with MockedSymStore(self, exe, sym) as dir:
+            with HTTPServer(dir) as url:
+                self.runCmd(f"settings set plugin.symbol-locator.symstore.urls {url}")
                 self.try_breakpoint(exe, should_have_loc=True)

>From 354668e537d4514a70024da111bd58aac56363f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Wed, 18 Mar 2026 14:45:45 +0100
Subject: [PATCH 7/7] ReportWarning instead of logging failed FS ops

---
 .../SymStore/SymbolLocatorSymStore.cpp        | 21 ++++++++++---------
 1 file changed, 11 insertions(+), 10 deletions(-)

diff --git a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
index 7df6198c837c4..1975e313ccdf9 100644
--- a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
+++ b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
@@ -190,25 +190,26 @@ requestFileFromSymStoreServerHTTP(llvm::StringRef base_url, llvm::StringRef key,
   using namespace llvm::sys;
   Log *log = GetLog(LLDBLog::Symbols);
 
-  //// Make sure URL will be valid, portable, and compatible with symbol servers
+  // Make sure URL will be valid, portable, and compatible with symbol servers
   if (has_unsafe_characters(pdb_name)) {
-    LLDB_LOGV(log,
-              "Rejecting HTTP lookup for PDB file due to unsafe characters in "
-              "name: {0}",
-              pdb_name);
+    Debugger::ReportWarning(llvm::formatv(
+        "Rejecting HTTP lookup for PDB file due to unsafe characters in "
+        "name: {0}",
+        pdb_name));
     return {};
   }
 
   // Construct the path for local storage. Configurable cache coming soon.
   llvm::SmallString<128> cache_file;
   if (!path::cache_directory(cache_file)) {
-    LLDB_LOGV(log, "Failed to determine cache directory for SymStore");
+    Debugger::ReportWarning("Failed to determine cache directory for SymStore");
     return {};
   }
   path::append(cache_file, "lldb", "SymStore", pdb_name, key);
   if (std::error_code ec = fs::create_directories(cache_file)) {
-    LLDB_LOG(log, "Failed to create temporary folder '{0}': {1}", cache_file,
-             ec.message());
+    Debugger::ReportWarning(
+        llvm::formatv("Failed to create cache directory '{0}': {1}", cache_file,
+                      ec.message()));
     return {};
   }
   path::append(cache_file, pdb_name);
@@ -218,7 +219,7 @@ requestFileFromSymStoreServerHTTP(llvm::StringRef base_url, llvm::StringRef key,
       llvm::formatv("{0}/{1}/{2}/{1}", base_url, pdb_name, key);
   if (llvm::Error err = downloadFileHTTP(source_url, cache_file.str())) {
     LLDB_LOG_ERROR(log, std::move(err),
-                   "  Failed to download from SymStore '{1}': {0}", source_url);
+                   "Failed to download from SymStore '{1}': {0}", source_url);
     return {};
   }
 
@@ -277,7 +278,7 @@ std::optional<FileSpec> SymbolLocatorSymStore::LocateExecutableSymbolFile(
   Args sym_store_urls = GetGlobalPluginProperties().GetURLs();
   for (const Args::ArgEntry &url : sym_store_urls) {
     if (auto spec = locateSymStoreEntry(url.ref(), key, pdb_name)) {
-      LLDB_LOGV(log, "  Found {0} in SymStore {1}", pdb_name, url.ref());
+      LLDB_LOGV(log, "Found {0} in SymStore {1}", pdb_name, url.ref());
       return *spec;
     }
   }



More information about the lldb-commits mailing list