[Lldb-commits] [lldb] [lldb] Fix process list race condition also on FreeBSD/OpenBSD (PR #205768)
Raphael Isemann via lldb-commits
lldb-commits at lists.llvm.org
Thu Jun 25 03:28:04 PDT 2026
https://github.com/Teemperor created https://github.com/llvm/llvm-project/pull/205768
cbd76dcfa81e58dbaca0 fixes a race condition when fetching process lists using sysctl. However, the same bug also exists also in the *BSD host libraries and in debugserver.
This patch merges the process list fetching logic of Darwin and the *BSD plugins and uses the race-free fetching approach.
I moved the shared logic into the Host/posix/ folder even though the sysctl call is not part of the POSIX standard. However, we don't have a better fitting existing folder that covers UNIX-like systems.
>From 34a4eba3dcf8b304e4cf761ff53504df9a8b14be Mon Sep 17 00:00:00 2001
From: Raphael Isemann <rise at apple.com>
Date: Thu, 18 Jun 2026 13:35:21 +0100
Subject: [PATCH] [lldb] Fix process list race condition also on
FreeBSD/OpenBSD
cbd76dcfa81e58dbaca0 fixes a race condition when fetching process lists
using sysctl. However, the same bug also exists also in the *BSD host
libraries and in debugserver.
This patch merges the process list fetching logic of Darwin and the
*BSD plugins and uses the race-free fetching approach.
I moved the shared logic into the Host/posix/ folder even though the
sysctl call is not part of the POSIX standard. However, we don't
have a better fitting existing folder that covers UNIX-like systems.
---
lldb/include/lldb/Host/posix/GetProcessList.h | 26 ++++++
lldb/source/Host/CMakeLists.txt | 3 +
lldb/source/Host/freebsd/Host.cpp | 26 ++----
lldb/source/Host/macosx/objcxx/Host.mm | 71 +--------------
lldb/source/Host/openbsd/Host.cpp | 25 ++----
lldb/source/Host/posix/GetProcessList.cpp | 89 +++++++++++++++++++
6 files changed, 134 insertions(+), 106 deletions(-)
create mode 100644 lldb/include/lldb/Host/posix/GetProcessList.h
create mode 100644 lldb/source/Host/posix/GetProcessList.cpp
diff --git a/lldb/include/lldb/Host/posix/GetProcessList.h b/lldb/include/lldb/Host/posix/GetProcessList.h
new file mode 100644
index 0000000000000..35b47d125f214
--- /dev/null
+++ b/lldb/include/lldb/Host/posix/GetProcessList.h
@@ -0,0 +1,26 @@
+//===-- GetProcessList.h ----------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_HOST_POSIX_GETPROCESSLIST_H
+#define LLDB_HOST_POSIX_GETPROCESSLIST_H
+
+#include "llvm/Support/Error.h"
+
+#include <sys/sysctl.h>
+#include <vector>
+
+namespace lldb_private {
+
+/// Fetches the list of all processes into \p kinfos using the BSD/Darwin
+/// sysctl(KERN_PROC_ALL) interface. Note that this is not technically a POSIX
+/// interface but used in several UNIX-like systems.
+llvm::Error GetProcessList(std::vector<struct kinfo_proc> &kinfos);
+
+} // namespace lldb_private
+
+#endif // LLDB_HOST_POSIX_GETPROCESSLIST_H
diff --git a/lldb/source/Host/CMakeLists.txt b/lldb/source/Host/CMakeLists.txt
index 28c639ee38215..11fc5b1c9ba04 100644
--- a/lldb/source/Host/CMakeLists.txt
+++ b/lldb/source/Host/CMakeLists.txt
@@ -123,6 +123,7 @@ else()
macosx/cfcpp/CFCMutableDictionary.cpp
macosx/cfcpp/CFCMutableSet.cpp
macosx/cfcpp/CFCString.cpp
+ posix/GetProcessList.cpp
)
if (LLDB_ENABLE_PYTHON)
add_host_subdirectory(macosx
@@ -157,6 +158,7 @@ else()
add_host_subdirectory(freebsd
freebsd/Host.cpp
freebsd/HostInfoFreeBSD.cpp
+ posix/GetProcessList.cpp
)
elseif (CMAKE_SYSTEM_NAME MATCHES "NetBSD")
@@ -169,6 +171,7 @@ else()
add_host_subdirectory(openbsd
openbsd/Host.cpp
openbsd/HostInfoOpenBSD.cpp
+ posix/GetProcessList.cpp
)
elseif (CMAKE_SYSTEM_NAME MATCHES "AIX")
diff --git a/lldb/source/Host/freebsd/Host.cpp b/lldb/source/Host/freebsd/Host.cpp
index dfdbfea0c3c0a..dada5c19653b2 100644
--- a/lldb/source/Host/freebsd/Host.cpp
+++ b/lldb/source/Host/freebsd/Host.cpp
@@ -21,9 +21,11 @@
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/Host.h"
#include "lldb/Host/HostInfo.h"
+#include "lldb/Host/posix/GetProcessList.h"
#include "lldb/Utility/DataBufferHeap.h"
#include "lldb/Utility/DataExtractor.h"
#include "lldb/Utility/Endian.h"
+#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/NameMatches.h"
#include "lldb/Utility/ProcessInfo.h"
@@ -170,30 +172,16 @@ uint32_t Host::FindProcessesImpl(const ProcessInstanceInfoMatch &match_info,
// Special case, if lldb is being run as root we can attach to anything.
bool all_users = match_info.GetMatchAllUsers() || (our_uid == 0);
- int mib[3] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL};
-
- size_t pid_data_size = 0;
- if (::sysctl(mib, 3, NULL, &pid_data_size, NULL, 0) != 0)
- return 0;
-
- // Add a few extra in case a few more show up
- const size_t estimated_pid_count =
- (pid_data_size / sizeof(struct kinfo_proc)) + 10;
-
- kinfos.resize(estimated_pid_count);
- pid_data_size = kinfos.size() * sizeof(struct kinfo_proc);
-
- if (::sysctl(mib, 3, &kinfos[0], &pid_data_size, NULL, 0) != 0)
+ if (llvm::Error error = GetProcessList(kinfos)) {
+ LLDB_LOG_ERROR(GetLog(LLDBLog::Host | LLDBLog::Process), std::move(error),
+ "failed to read process list: {0}");
return 0;
-
- const size_t actual_pid_count = (pid_data_size / sizeof(struct kinfo_proc));
+ }
ProcessInstanceInfoMatch match_info_noname{match_info};
match_info_noname.SetNameMatchType(NameMatch::Ignore);
- for (size_t i = 0; i < actual_pid_count; i++) {
- const struct kinfo_proc &kinfo = kinfos[i];
-
+ for (const struct kinfo_proc &kinfo : kinfos) {
/* Make sure the user is acceptable */
if (!all_users && kinfo.ki_ruid != our_uid)
continue;
diff --git a/lldb/source/Host/macosx/objcxx/Host.mm b/lldb/source/Host/macosx/objcxx/Host.mm
index 13ee1cefb6cea..05888c6c7e389 100644
--- a/lldb/source/Host/macosx/objcxx/Host.mm
+++ b/lldb/source/Host/macosx/objcxx/Host.mm
@@ -60,12 +60,13 @@
#include "lldb/Host/HostInfo.h"
#include "lldb/Host/ProcessLaunchInfo.h"
#include "lldb/Host/ThreadLauncher.h"
+#include "lldb/Host/posix/GetProcessList.h"
#include "lldb/Utility/ArchSpec.h"
-#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/DataBufferHeap.h"
#include "lldb/Utility/DataExtractor.h"
#include "lldb/Utility/Endian.h"
#include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/NameMatches.h"
#include "lldb/Utility/ProcessInfo.h"
@@ -688,74 +689,6 @@ static bool GetMacOSXProcessUserAndGroup(ProcessInstanceInfo &process_info) {
return false;
}
-/// Fetches the list of all processes into \p kinfos.
-static llvm::Error GetProcessList(std::vector<struct kinfo_proc> &kinfos) {
- int mib[3] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL};
-
- // How often we retry fetching the process list.
- static constexpr unsigned g_retry_count = 200;
- // The rate at which we increase the adjusted buffer size to
- // account for newly created processes between two sysctl calls.
- static constexpr unsigned g_expected_new_pids = 500;
- // We keep increasing the expected growth rate between the two
- // sysctl calls. Check that the last attempt does not create an
- // unreasonbly large buffer. It is unlikely we run on a system where
- // 100k processes are repeatedly created between each attempted sysctl
- // pair.
- static_assert(
- g_retry_count * g_expected_new_pids <= 100'000,
- "Final retry attempt assumes an unlikely amount of new processes.");
-
- // This is an inherently racy API. We have to first query the size for our
- // buffer and then pass it back to sysctl. If more processes spawn between the
- // size query and the actual call to fetch, then sysctl returns ENOMEM.
- // We keep retrying until we get a passing result.
- for (unsigned attempt = 1; attempt < g_retry_count; ++attempt) {
- // Fetch the buffer size sysctl would return.
- size_t current_pid_size = 0;
- if (::sysctl(mib, 3, nullptr, ¤t_pid_size, nullptr, 0) != 0)
- return llvm::errorCodeToError(
- std::error_code(errno, std::generic_category()));
-
- // Convert the byte length result to number of elements.
- const size_t current_num_processes =
- current_pid_size / sizeof(struct kinfo_proc);
-
- // Adjust the buffer for new processes that spawned between the
- // previous and next sysctl call. We increase this growth each attempt
- // to account for systems where a lot of new processes spawn between
- // these two calls.
- const size_t expected_growth = attempt * g_expected_new_pids;
-
- // Allocate the buffer for the sysctl result.
- kinfos.resize(current_num_processes + expected_growth);
-
- // Fetch the actual process list and let sysctl adjust actual_pid_size.
- size_t actual_pid_size = kinfos.size() * sizeof(struct kinfo_proc);
- if (::sysctl(mib, 3, &kinfos[0], &actual_pid_size, nullptr, 0) == 0) {
- // Shrink the buffer to the actual number of processes returned.
- kinfos.resize(actual_pid_size / sizeof(struct kinfo_proc));
- return llvm::Error::success();
- }
-
- // Errno is set to ENOMEM if our estimated_pid_size is too small, in which
- // case we retry with a bigger buffer. Any other error is unexpected.
- if (errno != ENOMEM)
- return llvm::errorCodeToError(
- std::error_code(errno, std::generic_category()));
- }
-
- // The only way to exit the loop above is by repeatedly hitting ENOMEM. The
- // only way this can happen is if the process list somehow grew extremely
- // large between the two sysctl calls.
- assert(errno == ENOMEM &&
- "loop should only be left via the ENOMEM retry path");
- return llvm::createStringErrorV(
- "Failed to read process list: sysctl kept returning ENOMEM after {0} "
- "attempts",
- g_retry_count);
-}
-
uint32_t Host::FindProcessesImpl(const ProcessInstanceInfoMatch &match_info,
ProcessInstanceInfoList &process_infos) {
std::vector<struct kinfo_proc> kinfos;
diff --git a/lldb/source/Host/openbsd/Host.cpp b/lldb/source/Host/openbsd/Host.cpp
index 2b66a3c8696b1..3bd68bd20fb6d 100644
--- a/lldb/source/Host/openbsd/Host.cpp
+++ b/lldb/source/Host/openbsd/Host.cpp
@@ -19,9 +19,11 @@
#include "lldb/Host/Host.h"
#include "lldb/Host/HostInfo.h"
+#include "lldb/Host/posix/GetProcessList.h"
#include "lldb/Utility/DataBufferHeap.h"
#include "lldb/Utility/DataExtractor.h"
#include "lldb/Utility/Endian.h"
+#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/NameMatches.h"
#include "lldb/Utility/ProcessInfo.h"
@@ -127,29 +129,16 @@ uint32_t Host::FindProcessesImpl(const ProcessInstanceInfoMatch &match_info,
ProcessInstanceInfoList &process_infos) {
std::vector<struct kinfo_proc> kinfos;
- int mib[3] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL};
-
- size_t pid_data_size = 0;
- if (::sysctl(mib, 3, NULL, &pid_data_size, NULL, 0) != 0)
+ if (llvm::Error error = GetProcessList(kinfos)) {
+ LLDB_LOG_ERROR(GetLog(LLDBLog::Host | LLDBLog::Process), std::move(error),
+ "failed to read process list: {0}");
return 0;
-
- // Add a few extra in case a few more show up
- const size_t estimated_pid_count =
- (pid_data_size / sizeof(struct kinfo_proc)) + 10;
-
- kinfos.resize(estimated_pid_count);
- pid_data_size = kinfos.size() * sizeof(struct kinfo_proc);
-
- if (::sysctl(mib, 3, &kinfos[0], &pid_data_size, NULL, 0) != 0)
- return 0;
-
- const size_t actual_pid_count = (pid_data_size / sizeof(struct kinfo_proc));
+ }
bool all_users = match_info.GetMatchAllUsers();
const ::pid_t our_pid = getpid();
const uid_t our_uid = getuid();
- for (size_t i = 0; i < actual_pid_count; i++) {
- const struct kinfo_proc &kinfo = kinfos[i];
+ for (const struct kinfo_proc &kinfo : kinfos) {
const bool kinfo_user_matches = (all_users || (kinfo.p_ruid == our_uid) ||
// Special case, if lldb is being run as
// root we can attach to anything.
diff --git a/lldb/source/Host/posix/GetProcessList.cpp b/lldb/source/Host/posix/GetProcessList.cpp
new file mode 100644
index 0000000000000..e6ce2814ff95c
--- /dev/null
+++ b/lldb/source/Host/posix/GetProcessList.cpp
@@ -0,0 +1,89 @@
+//===-- GetProcessList.cpp ------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Host/posix/GetProcessList.h"
+
+#include "llvm/Support/Error.h"
+#include "llvm/Support/ErrorExtras.h"
+
+#include <cassert>
+#include <cerrno>
+#include <cstddef>
+#include <sys/sysctl.h>
+#include <sys/types.h>
+// struct kinfo_proc lives in <sys/user.h> on some BSDs (e.g. FreeBSD).
+#include <sys/user.h>
+#include <system_error>
+
+llvm::Error
+lldb_private::GetProcessList(std::vector<struct kinfo_proc> &kinfos) {
+ int mib[3] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL};
+
+ // How often we retry fetching the process list.
+ static constexpr unsigned g_retry_count = 200;
+ // The rate at which we increase the adjusted buffer size to
+ // account for newly created processes between two sysctl calls.
+ static constexpr unsigned g_expected_new_pids = 500;
+ // We keep increasing the expected growth rate between the two
+ // sysctl calls. Check that the last attempt does not create an
+ // unreasonbly large buffer. It is unlikely we run on a system where
+ // 100k processes are repeatedly created between each attempted sysctl
+ // pair.
+ static_assert(
+ g_retry_count * g_expected_new_pids <= 100'000,
+ "Final retry attempt assumes an unlikely amount of new processes.");
+
+ // This is an inherently racy API. We have to first query the size for our
+ // buffer and then pass it back to sysctl. If more processes spawn between the
+ // size query and the actual call to fetch, then sysctl returns ENOMEM.
+ // We keep retrying until we get a passing result.
+ for (unsigned attempt = 1; attempt < g_retry_count; ++attempt) {
+ // Fetch the buffer size sysctl would return.
+ size_t current_pid_size = 0;
+ if (::sysctl(mib, 3, nullptr, ¤t_pid_size, nullptr, 0) != 0)
+ return llvm::errorCodeToError(
+ std::error_code(errno, std::generic_category()));
+
+ // Convert the byte length result to number of elements.
+ const size_t current_num_processes =
+ current_pid_size / sizeof(struct kinfo_proc);
+
+ // Adjust the buffer for new processes that spawned between the
+ // previous and next sysctl call. We increase this growth each attempt
+ // to account for systems where a lot of new processes spawn between
+ // these two calls.
+ const size_t expected_growth = attempt * g_expected_new_pids;
+
+ // Allocate the buffer for the sysctl result.
+ kinfos.resize(current_num_processes + expected_growth);
+
+ // Fetch the actual process list and let sysctl adjust actual_pid_size.
+ size_t actual_pid_size = kinfos.size() * sizeof(struct kinfo_proc);
+ if (::sysctl(mib, 3, &kinfos[0], &actual_pid_size, nullptr, 0) == 0) {
+ // Shrink the buffer to the actual number of processes returned.
+ kinfos.resize(actual_pid_size / sizeof(struct kinfo_proc));
+ return llvm::Error::success();
+ }
+
+ // Errno is set to ENOMEM if our estimated_pid_size is too small, in which
+ // case we retry with a bigger buffer. Any other error is unexpected.
+ if (errno != ENOMEM)
+ return llvm::errorCodeToError(
+ std::error_code(errno, std::generic_category()));
+ }
+
+ // The only way to exit the loop above is by repeatedly hitting ENOMEM. The
+ // only way this can happen is if the process list somehow grew extremely
+ // large between the two sysctl calls.
+ assert(errno == ENOMEM &&
+ "loop should only be left via the ENOMEM retry path");
+ return llvm::createStringErrorV(
+ "Failed to read process list: sysctl kept returning ENOMEM after {0} "
+ "attempts",
+ g_retry_count);
+}
More information about the lldb-commits
mailing list