[Lldb-commits] [lldb] [lldb][Darwin] Batch debugserver Mach-O load-command reads (PR #232684)
via lldb-commits
lldb-commits at lists.llvm.org
Fri Oct 16 05:36:07 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lldb
Author: Karim Alweheshy (karim-alweheshy)
<details>
<summary>Changes</summary>
**Why:** During attach, debugserver reads each image's Mach-O load-command headers and payloads separately. Targets with many dynamic libraries repeat this work across every image.
**Change:** Read commands through a bounded 16 KB window. Keep breakpoint-byte restoration and command validation; fall back to an exact read when optional data is unreadable. This PR changes the server-side traversal.
**Validation:** Standalone arm64 debugserver builds; four API cases pass with matching current-main LLDB and with Apple LLDB. They cover parsing, window boundaries, breakpoints, unreadable data and malformed commands. A 32-bit inferior has not been tested.
<details>
<summary>Prior reviews</summary>
- Excludes the platform-query hoist already proposed in #<!-- -->140853 and preserves retries to avoid the zero-platform cache problem in closed #<!-- -->140611.
- #<!-- -->200072 and #<!-- -->200515 optimize client-side reads; this handles debugserver's traversal.
- Uses standard-library storage to preserve standalone builds, following the dependency failure in [D131901](https://reviews.llvm.org/D131901).
</details>
Assisted-by: OpenAI Codex
---
Patch is 22.00 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/232684.diff
6 Files Affected:
- (modified) lldb/test/API/macosx/unregistered-macho/Makefile (+1-1)
- (modified) lldb/test/API/macosx/unregistered-macho/TestUnregisteredMacho.py (+109)
- (added) lldb/test/API/macosx/unregistered-macho/load-commands.c (+168)
- (modified) lldb/test/API/macosx/unregistered-macho/main.c (+3)
- (modified) lldb/tools/debugserver/source/MacOSX/MachProcess.h (+5)
- (modified) lldb/tools/debugserver/source/MacOSX/MachProcess.mm (+109-21)
``````````diff
diff --git a/lldb/test/API/macosx/unregistered-macho/Makefile b/lldb/test/API/macosx/unregistered-macho/Makefile
index 58c3a468659e33..7825454ac51501 100644
--- a/lldb/test/API/macosx/unregistered-macho/Makefile
+++ b/lldb/test/API/macosx/unregistered-macho/Makefile
@@ -1,3 +1,3 @@
-C_SOURCES = main.c
+C_SOURCES = main.c load-commands.c
include Makefile.rules
diff --git a/lldb/test/API/macosx/unregistered-macho/TestUnregisteredMacho.py b/lldb/test/API/macosx/unregistered-macho/TestUnregisteredMacho.py
index 11ff1d4e1efa7c..53804edc845b9b 100644
--- a/lldb/test/API/macosx/unregistered-macho/TestUnregisteredMacho.py
+++ b/lldb/test/API/macosx/unregistered-macho/TestUnregisteredMacho.py
@@ -1,5 +1,6 @@
"""Test that debugserver will parse a mach-o in inferior memory even if it's not loaded."""
+import json
import os
import re
import subprocess
@@ -60,3 +61,111 @@ def test(self):
substrs=['"load_address":%d,' % invalid_macho_addr],
matching=False,
)
+
+ def start_load_command_fixture(self):
+ self.build()
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.c")
+ )
+ return target, process
+
+ def image_address(self, target, name):
+ value = target.FindFirstGlobalVariable(name)
+ self.assertTrue(value.IsValid(), name)
+ return value.GetValueAsUnsigned()
+
+ def image_info(self, address):
+ request = "jGetLoadedDynamicLibrariesInfos:" + json.dumps(
+ {"solib_addresses": [address]}, separators=(",", ":")
+ )
+ return json.loads(lldbutil.send_packet_get_reply(self, request))["images"]
+
+ def check_prefixes(self, image, version):
+ self.assertEqual(image["pathname"], "")
+ self.assertEqual(image["uuid"], "12345678-1234-5678-ABCD-0123456789AB")
+ self.assertEqual(image["mach_header"]["magic"], 0xFEEDFACF)
+ self.assertEqual(image["mach_header"]["cpusubtype"], 5)
+ self.assertEqual(image["min_version_os_name"], "macosx")
+ self.assertEqual(image["min_version_os_sdk"], version)
+ self.assertEqual(
+ image["segments"],
+ [
+ {
+ "name": "1234567890abcdef",
+ "vmaddr": 0x123456789ABCDEF0,
+ "vmsize": 0x10000,
+ "fileoff": 0x1000,
+ "filesize": 0x2000,
+ "maxprot": 7,
+ },
+ {
+ "name": "__TEXT32",
+ "vmaddr": 0x5000,
+ "vmsize": 0x1000,
+ "fileoff": 0x20,
+ "filesize": 0x200,
+ "maxprot": 5,
+ },
+ ],
+ )
+
+ @no_debug_info_test
+ @skipUnlessDarwin
+ def test_load_command_prefixes_across_cache_boundaries(self):
+ target, process = self.start_load_command_fixture()
+ for name, version in [
+ ("crossing_image", "13.2.1"),
+ ("large_image", "10.12"),
+ ("unreadable_unknown_image", "13.2.1"),
+ ]:
+ address = self.image_address(target, name)
+ images = self.image_info(address)
+ self.assertEqual(len(images), 1, name)
+ self.assertEqual(images[0]["load_address"], address)
+ self.check_prefixes(images[0], version)
+
+ # MachProcess::ReadMemory must restore software breakpoint bytes in
+ # cached command data, just as it does for an individual memory read.
+ address = self.image_address(target, "breakpoint_address")
+ kind = 4 if "arm64" in self.getArchitecture() else 1
+ self.assertEqual(
+ lldbutil.send_packet_get_reply(self, f"Z0,{address:x},{kind}"), "OK"
+ )
+ try:
+ images = self.image_info(self.image_address(target, "crossing_image"))
+ self.check_prefixes(images[0], "13.2.1")
+ finally:
+ self.assertEqual(
+ lldbutil.send_packet_get_reply(self, f"z0,{address:x},{kind}"), "OK"
+ )
+
+ @no_debug_info_test
+ @skipUnlessDarwin
+ def test_unreadable_optional_load_command_payloads(self):
+ target, process = self.start_load_command_fixture()
+ for name in [
+ "unreadable_uuid_image",
+ "unreadable_version_image",
+ "huge_size_image",
+ ]:
+ images = self.image_info(self.image_address(target, name))
+ self.assertEqual(len(images), 1, name)
+ self.assertEqual(images[0]["segments"], [])
+ self.assertNotIn("min_version_os_name", images[0])
+ images = self.image_info(self.image_address(target, "unreadable_segment_image"))
+ self.assertEqual(images, [])
+
+ @no_debug_info_test
+ @skipUnlessDarwin
+ def test_malformed_load_command_prefixes(self):
+ target, process = self.start_load_command_fixture()
+ for name in [
+ "invalid_size_image",
+ "invalid_count_image",
+ "invalid_prefix_image",
+ "invalid_overrun_image",
+ ]:
+ self.assertEqual(
+ self.image_info(self.image_address(target, name)), [], name
+ )
+ self.assertEqual(self.image_info((1 << 64) - 8), [])
diff --git a/lldb/test/API/macosx/unregistered-macho/load-commands.c b/lldb/test/API/macosx/unregistered-macho/load-commands.c
new file mode 100644
index 00000000000000..03974c4621c0ed
--- /dev/null
+++ b/lldb/test/API/macosx/unregistered-macho/load-commands.c
@@ -0,0 +1,168 @@
+#include <assert.h>
+#include <mach-o/loader.h>
+#include <mach/machine.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include <uuid/uuid.h>
+
+uint8_t *crossing_image;
+uint8_t *large_image;
+uint8_t *unreadable_unknown_image;
+uint8_t *unreadable_uuid_image;
+uint8_t *unreadable_version_image;
+uint8_t *unreadable_segment_image;
+uint8_t *invalid_size_image;
+uint8_t *invalid_count_image;
+uint8_t *invalid_prefix_image;
+uint8_t *invalid_overrun_image;
+uint8_t *huge_size_image;
+uint8_t *breakpoint_address;
+
+static const uint32_t unknown_command = 0x12345678;
+
+static void write_header(uint8_t *buffer, uint32_t count, uint32_t size) {
+ struct mach_header_64 header = {0};
+ header.magic = MH_MAGIC_64;
+#if defined(__x86_64__)
+ header.cputype = CPU_TYPE_X86_64;
+#else
+ header.cputype = CPU_TYPE_ARM64;
+#endif
+ header.cpusubtype = CPU_SUBTYPE_MASK | 5;
+ header.filetype = MH_DYLIB;
+ header.ncmds = count;
+ header.sizeofcmds = size;
+ header.flags = MH_NOUNDEFS | MH_DYLDLINK;
+ memcpy(buffer, &header, sizeof(header));
+}
+
+static size_t write_prefixes(uint8_t *buffer, int legacy_version) {
+ uint8_t *cursor = buffer;
+ struct uuid_command uuid = {0};
+ uuid.cmd = LC_UUID;
+ uuid.cmdsize = sizeof(uuid);
+ uuid_parse("12345678-1234-5678-abcd-0123456789ab", uuid.uuid);
+ memcpy(cursor, &uuid, sizeof(uuid));
+ cursor += sizeof(uuid);
+
+ struct segment_command_64 segment64 = {0};
+ segment64.cmd = LC_SEGMENT_64;
+ segment64.cmdsize = sizeof(segment64);
+ memcpy(segment64.segname, "1234567890abcdef", 16);
+ segment64.vmaddr = 0x123456789abcdef0;
+ segment64.vmsize = 0x10000;
+ segment64.fileoff = 0x1000;
+ segment64.filesize = 0x2000;
+ segment64.maxprot = 7;
+ memcpy(cursor, &segment64, sizeof(segment64));
+ cursor += sizeof(segment64);
+
+ struct segment_command segment32 = {0};
+ segment32.cmd = LC_SEGMENT;
+ segment32.cmdsize = sizeof(segment32);
+ memcpy(segment32.segname, "__TEXT32", 8);
+ segment32.vmaddr = 0x5000;
+ segment32.vmsize = 0x1000;
+ segment32.fileoff = 0x20;
+ segment32.filesize = 0x200;
+ segment32.maxprot = 5;
+ memcpy(cursor, &segment32, sizeof(segment32));
+ cursor += sizeof(segment32);
+
+ if (legacy_version) {
+ struct version_min_command version = {0};
+ version.cmd = LC_VERSION_MIN_MACOSX;
+ version.cmdsize = sizeof(version);
+ version.version = (10 << 16) | (12 << 8);
+ memcpy(cursor, &version, sizeof(version));
+ cursor += sizeof(version);
+ } else {
+ struct build_version_command version = {0};
+ version.cmd = LC_BUILD_VERSION;
+ version.cmdsize = sizeof(version);
+ version.platform = PLATFORM_MACOS;
+ version.minos = (13 << 16) | (2 << 8) | 1;
+ memcpy(cursor, &version, sizeof(version));
+ cursor += sizeof(version);
+ }
+ return cursor - buffer;
+}
+
+// Put an unknown command before UUID/segment/version prefixes. The UUID starts
+// eight bytes before a 16 KB boundary, and its payload crosses that boundary.
+static uint8_t *make_crossing_image(uint32_t unknown_size, int legacy) {
+ uint8_t *buffer =
+ calloc(1, sizeof(struct mach_header_64) + unknown_size + 256);
+ assert(buffer);
+ uint8_t *commands = buffer + sizeof(struct mach_header_64);
+ struct load_command unknown = {unknown_command, unknown_size};
+ memcpy(commands, &unknown, sizeof(unknown));
+ size_t prefix_size = write_prefixes(commands + unknown_size, legacy);
+ write_header(buffer, 5, unknown_size + prefix_size);
+ return buffer;
+}
+
+static uint8_t *make_unreadable_payload(uint32_t command,
+ uint32_t command_size) {
+ size_t page_size = getpagesize();
+ uint8_t *mapping = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANON, -1, 0);
+ assert(mapping != MAP_FAILED);
+ uint8_t *buffer = mapping + page_size - sizeof(struct mach_header_64) -
+ sizeof(struct load_command);
+ write_header(buffer, 1, command_size);
+ struct load_command prefix = {command, command_size};
+ memcpy(buffer + sizeof(struct mach_header_64), &prefix, sizeof(prefix));
+ assert(mprotect(mapping + page_size, page_size, PROT_NONE) == 0);
+ return buffer;
+}
+
+static uint8_t *make_bad_image(uint32_t count, uint32_t size, uint32_t command,
+ uint32_t command_size) {
+ uint8_t *buffer = calloc(1, sizeof(struct mach_header_64) + 128);
+ assert(buffer);
+ write_header(buffer, count, size);
+ struct load_command prefix = {command, command_size};
+ memcpy(buffer + sizeof(struct mach_header_64), &prefix, sizeof(prefix));
+ return buffer;
+}
+
+void prepare_load_command_images(void) {
+ crossing_image = make_crossing_image(16 * 1024 - 8, 0);
+ breakpoint_address = crossing_image + sizeof(struct mach_header_64) +
+ (16 * 1024 - 8) + sizeof(struct load_command);
+ large_image = make_crossing_image(2 * 1024 * 1024, 1);
+
+ size_t page_size = getpagesize();
+ uint8_t *mapping = mmap(NULL, 3 * page_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANON, -1, 0);
+ assert(mapping != MAP_FAILED);
+ unreadable_unknown_image = mapping + page_size -
+ sizeof(struct mach_header_64) -
+ sizeof(struct load_command);
+ uint8_t *commands = unreadable_unknown_image + sizeof(struct mach_header_64);
+ uint32_t unknown_size = page_size + sizeof(struct load_command);
+ struct load_command unknown = {unknown_command, unknown_size};
+ memcpy(commands, &unknown, sizeof(unknown));
+ size_t prefix_size = write_prefixes(commands + unknown_size, 0);
+ write_header(unreadable_unknown_image, 5, unknown_size + prefix_size);
+ assert(mprotect(mapping + page_size, page_size, PROT_NONE) == 0);
+
+ unreadable_uuid_image =
+ make_unreadable_payload(LC_UUID, sizeof(struct uuid_command));
+ unreadable_version_image = make_unreadable_payload(
+ LC_BUILD_VERSION, sizeof(struct build_version_command));
+ unreadable_segment_image =
+ make_unreadable_payload(LC_SEGMENT_64, sizeof(struct segment_command_64));
+ invalid_size_image = make_bad_image(1, 8, unknown_command, 0);
+ invalid_count_image = make_bad_image(2, 8, unknown_command, 8);
+ invalid_prefix_image = make_bad_image(1, 8, LC_SEGMENT_64, 8);
+ invalid_overrun_image = make_bad_image(1, 8, unknown_command, 16);
+ // The declared region is intentionally huge and mostly inaccessible. Only
+ // the unknown command's header is required, so do not allocate or read it
+ // all.
+ huge_size_image = make_unreadable_payload(unknown_command, UINT32_MAX - 7);
+}
diff --git a/lldb/test/API/macosx/unregistered-macho/main.c b/lldb/test/API/macosx/unregistered-macho/main.c
index 5de4b5f6424677..865a5f8a75eccb 100644
--- a/lldb/test/API/macosx/unregistered-macho/main.c
+++ b/lldb/test/API/macosx/unregistered-macho/main.c
@@ -4,7 +4,10 @@
#include <string.h>
#include <uuid/uuid.h>
+void prepare_load_command_images(void);
+
int main() {
+ prepare_load_command_images();
int size_of_load_cmds =
sizeof(struct segment_command_64) + sizeof(struct uuid_command);
uint8_t *macho_buf =
diff --git a/lldb/tools/debugserver/source/MacOSX/MachProcess.h b/lldb/tools/debugserver/source/MacOSX/MachProcess.h
index 43852801a81696..5d936bf6db6f2f 100644
--- a/lldb/tools/debugserver/source/MacOSX/MachProcess.h
+++ b/lldb/tools/debugserver/source/MacOSX/MachProcess.h
@@ -258,6 +258,11 @@ class MachProcess {
DeploymentInfo GetDeploymentInfo(const struct load_command &,
uint64_t load_command_address,
bool is_executable);
+ // command_data contains the validated command prefix, or is null when that
+ // optional prefix could not be read from the inferior.
+ DeploymentInfo GetDeploymentInfo(const struct load_command &,
+ const uint8_t *command_data,
+ bool is_executable);
static std::optional<std::string> GetPlatformString(unsigned char platform);
bool GetMachOInformationFromMemory(uint32_t platform,
nub_addr_t mach_o_header_addr,
diff --git a/lldb/tools/debugserver/source/MacOSX/MachProcess.mm b/lldb/tools/debugserver/source/MacOSX/MachProcess.mm
index 6ca1945c7e595d..ff0b5aba080d2c 100644
--- a/lldb/tools/debugserver/source/MacOSX/MachProcess.mm
+++ b/lldb/tools/debugserver/source/MacOSX/MachProcess.mm
@@ -32,7 +32,9 @@
#include <uuid/uuid.h>
#include <algorithm>
+#include <array>
#include <chrono>
+#include <limits>
#include <map>
#include <unordered_set>
@@ -670,16 +672,42 @@ static bool FBSAddEventDataToOptions(NSMutableDictionary *options,
MachProcess::GetDeploymentInfo(const struct load_command &lc,
uint64_t load_command_address,
bool is_executable) {
+ size_t command_size = 0;
+ switch (lc.cmd & ~LC_REQ_DYLD) {
+ case LC_VERSION_MIN_IPHONEOS:
+ case LC_VERSION_MIN_MACOSX:
+ case LC_VERSION_MIN_TVOS:
+ case LC_VERSION_MIN_WATCHOS:
+ command_size = sizeof(struct version_min_command);
+ break;
+#if defined(LC_BUILD_VERSION)
+ case LC_BUILD_VERSION:
+ command_size = sizeof(struct build_version_command);
+ break;
+#endif
+ }
+ std::array<uint8_t, sizeof(struct segment_command_64)> command_data;
+ const uint8_t *data = nullptr;
+ if (command_size && ReadMemory(load_command_address, command_size,
+ command_data.data()) == command_size)
+ data = command_data.data();
+ return GetDeploymentInfo(lc, data, is_executable);
+}
+
+MachProcess::DeploymentInfo
+MachProcess::GetDeploymentInfo(const struct load_command &lc,
+ const uint8_t *command_data,
+ bool is_executable) {
DeploymentInfo info;
uint32_t cmd = lc.cmd & ~LC_REQ_DYLD;
// Handle the older LC_VERSION load commands, which don't
// distinguish between simulator and real hardware.
auto handle_version_min = [&](char platform) {
- struct version_min_command vers_cmd;
- if (ReadMemory(load_command_address, sizeof(struct version_min_command),
- &vers_cmd) != sizeof(struct version_min_command))
+ if (!command_data)
return;
+ struct version_min_command vers_cmd;
+ memcpy(&vers_cmd, command_data, sizeof(vers_cmd));
info.platform = platform;
info.major_version = vers_cmd.version >> 16;
info.minor_version = (vers_cmd.version >> 8) & 0xffu;
@@ -723,10 +751,10 @@ static bool FBSAddEventDataToOptions(NSMutableDictionary *options,
break;
#if defined(LC_BUILD_VERSION)
case LC_BUILD_VERSION: {
- struct build_version_command build_vers;
- if (ReadMemory(load_command_address, sizeof(struct build_version_command),
- &build_vers) != sizeof(struct build_version_command))
+ if (!command_data)
break;
+ struct build_version_command build_vers;
+ memcpy(&build_vers, command_data, sizeof(build_vers));
info.platform = build_vers.platform;
info.major_version = build_vers.minos >> 16;
info.minor_version = (build_vers.minos >> 8) & 0xffu;
@@ -814,6 +842,10 @@ static bool mach_header_validity_test(uint32_t magic, uint32_t cputype) {
uint32_t dyld_platform, nub_addr_t mach_o_header_addr, int wordsize,
struct mach_o_information &inf) {
uint64_t load_cmds_p;
+ const size_t header_size = wordsize == 4 ? sizeof(struct mach_header)
+ : sizeof(struct mach_header_64);
+ if (mach_o_header_addr > std::numeric_limits<nub_addr_t>::max() - header_size)
+ return false;
if (wordsize == 4) {
struct mach_header header;
@@ -853,18 +885,75 @@ static bool mach_header_validity_test(uint32_t magic, uint32_t cputype) {
inf.mach_header.sizeofcmds = header.sizeofcmds;
inf.mach_header.flags = header.flags;
}
+ const uint64_t load_cmds_size = inf.mach_header.sizeofcmds;
+ if (inf.mach_header.ncmds > load_cmds_size / sizeof(struct load_command) ||
+ load_cmds_p > std::numeric_limits<nub_addr_t>::max() - load_cmds_size)
+ return false;
+ const nub_addr_t load_cmds_end = load_cmds_p + load_cmds_size;
+
+ // Cache only a bounded part of this image's load commands. A large valid
+ // region needs more refills, rather than an allocation based on sizeofcmds.
+ std::array<uint8_t, 16 * 1024> cache;
+ nub_addr_t cache_address = load_cmds_p;
+ size_t cache_size = 0;
+ auto read_command = [&](nub_addr_t address, size_t size, void *destination) {
+ if (address > load_cmds_end || size > load_cmds_end - address)
+ return false;
+ if (address < cache_address || address - cache_address > cache_size ||
+ size > cache_size - (address - cache_address)) {
+ cache_address = address;
+ const size_t requested =
+ std::min<uint64_t>(cache.size(), load_cmds_end - address);
+ cache_size = ReadMemory(address, requested, cache.data());
+ if (cache_size < size) {
+ // An unknown command's payload need not be readable. If a bulk read
+ // crosses unreadable memory, still try the exact required prefix.
+ return ReadMemory(address, size, destination) == size;
+ }
+ }
+ memcpy(destination, cache.data() + (address - cache_address), size);
+ return true;
+ };
+
for (uint32_t j = 0; j < inf.mach_header.ncmds; j++) {
struct load_command lc;
- if (ReadMemory(load_cmds_p, sizeof(struct load_command), &lc) !=
- sizeof(struct load_command)) {
+ if (!read_command(load_cmds_p, sizeof(lc), &lc))
+ return false;
+ if (lc.cmdsize < sizeof(lc) || lc.cmdsize > load_cmds_end - load_cmds_p)
return false;
+
+ size_t command_size = sizeof(lc);
+ if (lc.cmd == LC_SEGMENT)
+ command_size = sizeof(struct segment_command);
+ else if (lc.cmd == LC_SEGMENT_64)
+ command_size = sizeof(struct segment_command_64);
+ else if (lc.cmd == LC_UUID)
+ command_size = sizeof(struct uuid_command);
+ else {
+ switch (lc.cmd & ~LC_REQ_DYLD) {
+ case LC_VERSION_MIN_IPHONEOS:
+ case LC_VERSION_MIN_MACOSX:
+ case LC_VERSION_MIN_TVOS:
+ case LC_VERSION_MIN_WATCHOS:
+ command_size = sizeof(struct version_min_command);
+ break;
+#if defined(LC_BUILD_VERSION)
+ case LC_BUILD_VERSION:
+ command_size = sizeof(struct build_version_command);
+ break;
+#endif
+ }
}
+ if (command_size > lc.cmdsize)
+ return false;
+ std::array<uint8_t, sizeof(struct segment_command_64)> command_data;
+ const bool have_command_data =
+ read_command(load_cmds_p, command_size, command_data.data());
if...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/232684
More information about the lldb-commits
mailing list