[Lldb-commits] [lldb] [llvm] [lldb] Add HTTPS hardening tests for SymbolLocatorSymStore (PR #192274)

Stefan Gränitz via lldb-commits lldb-commits at lists.llvm.org
Wed Apr 15 08:20:46 PDT 2026


https://github.com/weliveindetail created https://github.com/llvm/llvm-project/pull/192274

This patch proposes a way to test security features on the HTTPS path in SymbolLocatorSymStore. The key requirement is a valid certificate. Any real certificate from an authority trusted by the system certificate store would eventually expire and in our isolated test environment we cannot create something like that on the fly. Using self-signed certificates is the only way forward for testing. As we don't want to allow any arbitrary certificate either, we add a new property that pins a fingerprint and any self-signed certificate is only accepted if it matches this fingerprint.

>From f49305e6fb7b46182564b609af24fa6e992f12f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= <stefan.graenitz at gmail.com>
Date: Wed, 15 Apr 2026 13:08:27 +0200
Subject: [PATCH] [lldb] Add HTTPS hardening tests for SymbolLocatorSymStore

---
 .../SymStore/SymbolLocatorSymStore.cpp        |  22 ++-
 .../SymbolLocatorSymStoreProperties.td        |   3 +
 lldb/test/API/symstore/TestSymStore.py        | 177 +++++++++++++++++-
 llvm/include/llvm/HTTP/HTTPClient.h           |   4 +
 llvm/lib/HTTP/CMakeLists.txt                  |   2 +-
 llvm/lib/HTTP/HTTPClient.cpp                  |  66 ++++++-
 6 files changed, 266 insertions(+), 8 deletions(-)

diff --git a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
index 1344232917b33..86315c0305ccf 100644
--- a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
+++ b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
@@ -58,6 +58,25 @@ class PluginProperties : public Properties {
     m_collection_sp->GetPropertyAtIndexAsArgs(ePropertySymStoreURLs, urls);
     return urls;
   }
+
+  std::optional<std::string> GetTLSCertFingerprint() const {
+    OptionValueString *s =
+        m_collection_sp->GetPropertyAtIndexAsOptionValueString(
+            ePropertyTLSCertFingerprint);
+    if (!s)
+      return {};
+    llvm::StringRef val = s->GetCurrentValueAsRef();
+    if (val.empty())
+      return {};
+    if (val.size() != 64 || !llvm::all_of(val, llvm::isHexDigit)) {
+      Debugger::ReportWarning(llvm::formatv(
+          "plugin.symbol-locator.symstore.tls-cert-fingerprint: expected a "
+          "64-character hex string (SHA-256), but got '{0}', ignoring",
+          val));
+      return {};
+    }
+    return val.lower();
+  }
 };
 
 } // namespace
@@ -66,7 +85,6 @@ static PluginProperties &GetGlobalPluginProperties() {
   static PluginProperties g_settings;
   return g_settings;
 }
-
 SymbolLocatorSymStore::SymbolLocatorSymStore() : SymbolLocator() {}
 
 void SymbolLocatorSymStore::Initialize() {
@@ -184,6 +202,8 @@ RequestFileFromSymStoreServerHTTP(llvm::StringRef base_url, llvm::StringRef key,
       client);
 
   llvm::HTTPRequest request(source_url);
+  request.PinnedCertFingerprint =
+      GetGlobalPluginProperties().GetTLSCertFingerprint();
   if (llvm::Error Err = client.perform(request, Handler)) {
     Debugger::ReportWarning(
         llvm::formatv("failed to download from SymStore '{0}': {1}", source_url,
diff --git a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStoreProperties.td b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStoreProperties.td
index 0cd631a80b90b..7d9b1cbae5f8e 100644
--- a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStoreProperties.td
+++ b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStoreProperties.td
@@ -4,4 +4,7 @@ let Definition = "symbollocatorsymstore", Path = "plugin.symbol-locator.symstore
   def SymStoreURLs : Property<"urls", "Array">,
     ElementType<"String">,
     Desc<"List of local symstore directories to query for symbols">;
+  def TLSCertFingerprint : Property<"tls-cert-fingerprint", "String">,
+    DefaultStringValue<"">,
+    Desc<"SHA-256 fingerprint (lowercase hex, no separators) of the symbol server's TLS certificate. When set, LLDB will accept an HTTPS server that presents this self-signed certificate even if it is not trusted by the system certificate store (Windows only).">;
 }
diff --git a/lldb/test/API/symstore/TestSymStore.py b/lldb/test/API/symstore/TestSymStore.py
index b5c8fbb58324f..34c0ac186f49b 100644
--- a/lldb/test/API/symstore/TestSymStore.py
+++ b/lldb/test/API/symstore/TestSymStore.py
@@ -1,7 +1,11 @@
+import datetime
 import http.server
+import ipaddress
 import os
 import shutil
 import socketserver
+import ssl
+import sys
 import threading
 from functools import partial
 
@@ -90,6 +94,119 @@ def __exit__(self, *exc_info):
             self._thread.join()
 
 
+class HTTPSServer:
+    """
+    Context Manager to serve a local directory tree via HTTPS.
+    """
+
+    class ErrorAwareTCPServer(socketserver.ThreadingTCPServer):
+        """TCP layer that will suppress errors of the given type."""
+
+        def __init__(self, address, handler, err):
+            self.err = err
+            super().__init__(address, handler)
+
+        def handle_error(self, request, client_address):
+            if isinstance(sys.exc_info()[1], self.err):
+                return
+            super().handle_error(request, client_address)
+
+    def __init__(self, dir=None, handler=None, cert=None):
+        if handler is None:
+            handler = partial(http.server.SimpleHTTPRequestHandler, directory=dir)
+        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+        ctx.load_cert_chain(cert.file, cert.key_file)
+        address = ("localhost", 0)  # auto-select free port
+        self._server = self.ErrorAwareTCPServer(address, handler, ssl.SSLError)
+        self._server.socket = ctx.wrap_socket(self._server.socket, server_side=True)
+        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"https://{host}:{port}"
+
+    def __exit__(self, *exc_info):
+        if self._server:
+            self._server.shutdown()
+            self._server.server_close()
+        if self._thread:
+            self._thread.join()
+
+
+# FIXME: Drop this once the package is rolled out on all build bots
+def skipUnlessPackageAvailable(name):
+    available = True
+    try:
+        __import__(name)
+    except ImportError:
+        available = False
+    import unittest
+
+    return unittest.skipUnless(available, f"requires the '{name}' package")
+
+
+class SelfSignedCert:
+    """
+    Self-signed cert/key pair for localhost.
+    """
+
+    def __init__(self, tmpdir):
+        from cryptography import x509
+        from cryptography.x509.oid import NameOID
+        from cryptography.hazmat.primitives import hashes, serialization
+        from cryptography.hazmat.primitives.asymmetric import rsa
+
+        key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+        name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "localhost")])
+        now = datetime.datetime.now(datetime.timezone.utc)
+        cert = (
+            x509.CertificateBuilder()
+            .subject_name(name)
+            .issuer_name(name)
+            .public_key(key.public_key())
+            .serial_number(x509.random_serial_number())
+            .not_valid_before(now)
+            .not_valid_after(now + datetime.timedelta(days=1))
+            .add_extension(
+                x509.SubjectAlternativeName(
+                    [
+                        x509.DNSName("localhost"),
+                        x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
+                    ]
+                ),
+                critical=False,
+            )
+            .sign(key, hashes.SHA256())
+        )
+        os.makedirs(tmpdir, exist_ok=False)
+        self.key_file = os.path.join(tmpdir, "server.key")
+        self.file = os.path.join(tmpdir, "server.crt")
+        with open(self.key_file, "wb") as f:
+            f.write(
+                key.private_bytes(
+                    serialization.Encoding.PEM,
+                    serialization.PrivateFormat.TraditionalOpenSSL,
+                    serialization.NoEncryption(),
+                )
+            )
+        with open(self.file, "wb") as f:
+            f.write(cert.public_bytes(serialization.Encoding.PEM))
+        self.fingerprint = cert.fingerprint(hashes.SHA256()).hex()
+
+
+class RedirectHandler(http.server.BaseHTTPRequestHandler):
+    base_url = None
+
+    def do_GET(self):
+        self.send_response(301)
+        self.send_header("Location", self.base_url + self.path)
+        self.end_headers()
+
+    def log_message(self, *args):
+        pass  # suppress request logs
+
+
 class SymStoreTests(TestBase):
     TEST_WITH_PDB_DEBUG_INFO = True
 
@@ -157,8 +274,6 @@ def test_http_not_found(self):
                     warnings = err_file.read().decode()
                 self.assertEqual(warnings, "")
 
-    # TODO: Add test coverage for common HTTPS security scenarios, e.g. self-signed
-    # certs, non-HTTPS redirects, etc.
     def test_http(self):
         """
         Check that breakpoint hits with remote SymStore.
@@ -168,3 +283,61 @@ def test_http(self):
             with HTTPServer(dir) as url:
                 self.runCmd(f"settings set plugin.symbol-locator.symstore.urls {url}")
                 self.try_breakpoint(exe, should_have_loc=True)
+
+    @skipUnlessPackageAvailable("cryptography")
+    def test_https(self):
+        """
+        Check that breakpoint resolves with remote SymStore via HTTPS.
+        """
+        exe, sym = self.build_inferior()
+        with MockedSymStore(self, exe, sym) as symstore_dir:
+            cert = SelfSignedCert(self.getBuildArtifact("cert"))
+            with HTTPSServer(dir=symstore_dir, cert=cert) as https_url:
+                # We accept only the self-signed certificate with this fingerprint
+                self.runCmd(
+                    f"settings set plugin.symbol-locator.symstore.tls-cert-fingerprint {cert.fingerprint}"
+                )
+                self.runCmd(
+                    f"settings set plugin.symbol-locator.symstore.urls {https_url}"
+                )
+                self.try_breakpoint(exe, should_have_loc=True)
+
+    @skipUnlessPackageAvailable("cryptography")
+    def test_https_reject_selfsigned_cert(self):
+        """
+        Check that LLDB rejects an HTTPS server with an untrusted self-signed cert.
+        """
+        exe, sym = self.build_inferior()
+        with MockedSymStore(self, exe, sym) as symstore_dir:
+            cert = SelfSignedCert(self.getBuildArtifact("cert"))
+            with HTTPSServer(dir=symstore_dir, cert=cert) as https_url:
+                # No fingerprint set
+                self.runCmd(
+                    f"settings set plugin.symbol-locator.symstore.urls {https_url}"
+                )
+                self.try_breakpoint(exe, should_have_loc=False)
+                # Incorrect fingerprint set
+                bogus = "DEADBEEFCAFEBABE"
+                self.runCmd(
+                    f"settings set plugin.symbol-locator.symstore.tls-cert-fingerprint {bogus}{bogus}{bogus}{bogus}"
+                )
+                self.try_breakpoint(exe, should_have_loc=False)
+
+    @skipUnlessPackageAvailable("cryptography")
+    def test_https_reject_redirect_http(self):
+        """
+        Check that LLDB does not retrieve symbols from servers that redirect with security downgrades.
+        """
+        exe, sym = self.build_inferior()
+        with MockedSymStore(self, exe, sym) as symstore_dir:
+            with HTTPServer(symstore_dir) as http_url:
+                RedirectHandler.base_url = http_url
+                cert = SelfSignedCert(self.getBuildArtifact("cert"))
+                with HTTPSServer(handler=RedirectHandler, cert=cert) as https_url:
+                    self.runCmd(
+                        f"settings set plugin.symbol-locator.symstore.urls {https_url}"
+                    )
+                    self.runCmd(
+                        f"settings set plugin.symbol-locator.symstore.tls-cert-fingerprint {cert.fingerprint}"
+                    )
+                    self.try_breakpoint(exe, should_have_loc=False)
diff --git a/llvm/include/llvm/HTTP/HTTPClient.h b/llvm/include/llvm/HTTP/HTTPClient.h
index a2ba4251d35ce..a3db41c1a9034 100644
--- a/llvm/include/llvm/HTTP/HTTPClient.h
+++ b/llvm/include/llvm/HTTP/HTTPClient.h
@@ -21,6 +21,7 @@
 #include "llvm/Support/MemoryBuffer.h"
 
 #include <chrono>
+#include <optional>
 
 namespace llvm {
 
@@ -31,7 +32,10 @@ struct HTTPRequest {
   SmallString<128> Url;
   SmallVector<std::string, 0> Headers;
   HTTPMethod Method = HTTPMethod::GET;
+  // Follow redirects without security downgrades.
   bool FollowRedirects = true;
+  // Allow self-signed TLS certificates with this SHA-256 (WinHTTP only).
+  std::optional<std::string> PinnedCertFingerprint;
   HTTPRequest(StringRef Url);
 };
 
diff --git a/llvm/lib/HTTP/CMakeLists.txt b/llvm/lib/HTTP/CMakeLists.txt
index e3b71434333a1..9dab6ad200066 100644
--- a/llvm/lib/HTTP/CMakeLists.txt
+++ b/llvm/lib/HTTP/CMakeLists.txt
@@ -10,7 +10,7 @@ endif()
 
 # Use WinHTTP on Windows
 if (WIN32)
-  set(imported_libs ${imported_libs} winhttp.lib)
+  set(imported_libs ${imported_libs} winhttp.lib crypt32.lib)
 endif()
 
 # This is no component library so the LLVM dylib does not gain a dependency on curl
diff --git a/llvm/lib/HTTP/HTTPClient.cpp b/llvm/lib/HTTP/HTTPClient.cpp
index 8517c8cc7c7a0..c69aecfa7d619 100644
--- a/llvm/lib/HTTP/HTTPClient.cpp
+++ b/llvm/lib/HTTP/HTTPClient.cpp
@@ -33,7 +33,8 @@ HTTPRequest::HTTPRequest(StringRef Url) { this->Url = Url.str(); }
 
 bool operator==(const HTTPRequest &A, const HTTPRequest &B) {
   return A.Url == B.Url && A.Method == B.Method &&
-         A.FollowRedirects == B.FollowRedirects;
+         A.FollowRedirects == B.FollowRedirects &&
+         A.PinnedCertFingerprint == B.PinnedCertFingerprint;
 }
 
 HTTPResponseHandler::~HTTPResponseHandler() = default;
@@ -144,8 +145,13 @@ unsigned HTTPClient::responseCode() {
 #else
 
 #ifdef _WIN32
+
+// We cannot sort these headers alphabetically.
+// clang-format off
 #include <windows.h>
+#include <wincrypt.h>
 #include <winhttp.h>
+// clang-format on
 
 namespace {
 
@@ -236,6 +242,41 @@ void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {
   }
 }
 
+static Error VerifyTLSCertWinHTTP(HINTERNET RequestHandle,
+                                  const std::string &PinnedFingerprint) {
+  // Decode the expected fingerprint from hex into binary.
+  BYTE Expected[32];
+  DWORD ExpectedSize = sizeof(Expected);
+  if (!CryptStringToBinaryA(
+          PinnedFingerprint.c_str(), (DWORD)PinnedFingerprint.size(),
+          CRYPT_STRING_HEXRAW, Expected, &ExpectedSize, nullptr, nullptr))
+    return createStringError(errc::invalid_argument,
+                             "Invalid certificate fingerprint format");
+
+  // Retrieve the server certificate and compute its SHA-256 hash.
+  PCCERT_CONTEXT CertCtx = nullptr;
+  DWORD CertCtxSize = sizeof(CertCtx);
+  if (!WinHttpQueryOption(RequestHandle, WINHTTP_OPTION_SERVER_CERT_CONTEXT,
+                          &CertCtx, &CertCtxSize))
+    return createStringError(errc::io_error,
+                             "Failed to retrieve server certificate");
+
+  std::array<BYTE, 32> Actual;
+  DWORD ActualSize = Actual.size();
+  bool GotHash = CertGetCertificateContextProperty(
+      CertCtx, CERT_SHA256_HASH_PROP_ID, Actual.data(), &ActualSize);
+  CertFreeCertificateContext(CertCtx);
+  if (!GotHash)
+    return createStringError(errc::io_error,
+                             "Failed to compute certificate fingerprint");
+
+  if (memcmp(Actual.data(), Expected, Actual.size()) != 0)
+    return createStringError(errc::permission_denied,
+                             "Certificate fingerprint mismatch");
+
+  return Error::success();
+}
+
 Error HTTPClient::perform(const HTTPRequest &Request,
                           HTTPResponseHandler &Handler) {
   if (Request.Method != HTTPMethod::GET)
@@ -272,6 +313,14 @@ Error HTTPClient::perform(const HTTPRequest &Request,
                         &SecureProtocols, sizeof(SecureProtocols)))
     return createStringError(errc::io_error, "Failed to set secure protocols");
 
+  // Disallow redirects in general or HTTPS to HTTP only.
+  DWORD RedirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP;
+  if (!Request.FollowRedirects)
+    RedirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
+  if (!WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_REDIRECT_POLICY,
+                        &RedirectPolicy, sizeof(RedirectPolicy)))
+    return createStringError(errc::io_error, "Failed to set redirect policy");
+
   // Use HTTP/2 if available
   DWORD EnableHttp2 = WINHTTP_PROTOCOL_FLAG_HTTP2;
   WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL,
@@ -296,6 +345,7 @@ Error HTTPClient::perform(const HTTPRequest &Request,
   if (!Session->RequestHandle)
     return createStringError(errc::io_error, "Failed to open HTTP request");
 
+  DWORD SecurityFlags = 0;
   if (Secure) {
     // Enforce checks that certificate wasn't revoked.
     DWORD EnableRevocationChecks = WINHTTP_ENABLE_SSL_REVOCATION;
@@ -305,9 +355,11 @@ Error HTTPClient::perform(const HTTPRequest &Request,
       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;
+    // Bypass certificate chain validation with pinned certificates so
+    // that self-signed certificates are accepted at the WinHTTP level. Manual
+    // verification happens right after receiving the response.
+    if (Request.PinnedCertFingerprint)
+      SecurityFlags = (SecurityFlags | SECURITY_FLAG_IGNORE_UNKNOWN_CA);
     if (!WinHttpSetOption(Session->RequestHandle, WINHTTP_OPTION_SECURITY_FLAGS,
                           &SecurityFlags, sizeof(SecurityFlags)))
       return createStringError(errc::io_error,
@@ -333,6 +385,12 @@ Error HTTPClient::perform(const HTTPRequest &Request,
   if (!WinHttpReceiveResponse(Session->RequestHandle, nullptr))
     return createStringError(errc::io_error, "Failed to receive HTTP response");
 
+  // Verify the server certificate fingerprint if one was pinned.
+  if ((SecurityFlags & SECURITY_FLAG_IGNORE_UNKNOWN_CA) != 0)
+    if (Error Err = VerifyTLSCertWinHTTP(Session->RequestHandle,
+                                         *Request.PinnedCertFingerprint))
+      return Err;
+
   // Get response code
   DWORD CodeSize = sizeof(Session->ResponseCode);
   if (!WinHttpQueryHeaders(Session->RequestHandle,



More information about the lldb-commits mailing list