[Lldb-commits] [lldb] [lldb][Mach-O] Accept a zlib-compressed "process metadata" LC_NOTE (PR #231745)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Wed Oct 14 20:51:27 PDT 2026
https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/231745
>From eff37855bbdcd86d9071eb67c949875af0350acf Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Tue, 13 Oct 2026 21:41:46 -0700
Subject: [PATCH] [lldb][Mach-O] Accept zlib-compressed LC_NOTE payloads
LC_NOTE payloads such as "process metadata" can be large and compress
well. A compressed payload uses the layout of a GNU .zdebug section: the
magic "ZLIB", the uncompressed size as a big-endian uint64_t, then an
RFC 1950 zlib stream. Uncompressed payloads keep working. Only "process
metadata" reads its payload through this path for now.
Alternatives considered:
- zstd and xz compress better, but neither is available in every LLDB
build.
- libcompression codecs are Darwin-only.
- An ELF Elf64_Chdr has no magic, so detection would be inferred from
the data, which only holds for JSON.
- A separate note name for compressed payloads would make every reader
look for both names.
The size comes from the file, so it is checked against deflate's maximum
ratio before anything is allocated.
rdar://189409232
---
.../ObjectFile/Mach-O/ObjectFileMachO.cpp | 68 +++++++--
.../TestMetadataRegisters.py | 144 ++++++++++++------
2 files changed, 157 insertions(+), 55 deletions(-)
diff --git a/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp b/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp
index 4570df57c42cb3..7a88637e729f52 100644
--- a/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp
+++ b/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp
@@ -48,6 +48,8 @@
#include "lldb/Host/SafeMachO.h"
#include "llvm/ADT/DenseSet.h"
+#include "llvm/Support/Compression.h"
+#include "llvm/Support/Endian.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/MemoryBuffer.h"
@@ -5385,6 +5387,54 @@ bool ObjectFileMachO::GetCorefileThreadExtraInfos(
return false;
}
+/// Return the payload of an LC_NOTE, decompressing it if needed.
+///
+/// A payload is either stored as is, or compressed in the layout of a GNU
+/// .zdebug section:
+///
+/// offset size contents
+/// 0 4 magic "ZLIB"
+/// 4 8 uncompressed size, big-endian uint64_t
+/// 12 n RFC 1950 zlib stream
+///
+/// The size is big-endian regardless of the corefile's byte order. An
+/// uncompressed payload must not start with the magic.
+static llvm::Expected<std::string> ReadLCNotePayload(const DataExtractor &data,
+ lldb::offset_t offset,
+ uint64_t size) {
+ const uint8_t *bytes = data.PeekData(offset, size);
+ if (!bytes)
+ return llvm::createStringError(
+ "payload of %" PRIu64 " bytes is out of bounds", size);
+ llvm::ArrayRef<uint8_t> payload(bytes, size);
+
+ constexpr llvm::StringLiteral magic = "ZLIB";
+ constexpr size_t header_size = magic.size() + sizeof(uint64_t);
+ if (!llvm::toStringRef(payload).starts_with(magic))
+ return llvm::toStringRef(payload).str();
+ if (payload.size() < header_size)
+ return llvm::createStringError("compressed payload header is truncated");
+
+ uint64_t uncompressed_size =
+ llvm::support::endian::read64be(payload.data() + magic.size());
+ llvm::ArrayRef<uint8_t> compressed = payload.drop_front(header_size);
+ // Deflate cannot exceed a 1032:1 ratio. A larger claimed size is corrupt and
+ // must not size the allocation.
+ if (uncompressed_size / 1032 > compressed.size())
+ return llvm::createStringError("uncompressed size %" PRIu64
+ " is impossible for %zu compressed bytes",
+ uncompressed_size, compressed.size());
+ if (!llvm::compression::zlib::isAvailable())
+ return llvm::createStringError(
+ "payload is zlib-compressed but LLDB was built without zlib");
+
+ llvm::SmallVector<uint8_t, 0> uncompressed;
+ if (llvm::Error err = llvm::compression::zlib::decompress(
+ compressed, uncompressed, uncompressed_size))
+ return err;
+ return llvm::toStringRef(uncompressed).str();
+}
+
StructuredData::ObjectSP ObjectFileMachO::GetCorefileProcessMetadata() {
ModuleSP module_sp(GetModule());
if (!module_sp)
@@ -5401,18 +5451,16 @@ StructuredData::ObjectSP ObjectFileMachO::GetCorefileProcessMetadata() {
log,
"Multiple 'process metadata' LC_NOTEs found, only using the first.");
- auto [payload_offset, strsize] = lc_notes[0];
- std::string buf(strsize, '\0');
- if (m_data_nsp->CopyData(payload_offset, strsize, buf.data()) != strsize) {
- LLDB_LOGF(log,
- "Unable to read %" PRIu64
- " bytes of 'process metadata' LC_NOTE JSON contents",
- strsize);
+ auto [payload_offset, payload_size] = lc_notes[0];
+ llvm::Expected<std::string> json =
+ ReadLCNotePayload(*m_data_nsp, payload_offset, payload_size);
+ if (!json) {
+ LLDB_LOG_ERROR(log, json.takeError(),
+ "Unable to read 'process metadata' LC_NOTE: {0}");
return {};
}
- while (buf.back() == '\0')
- buf.resize(buf.size() - 1);
- StructuredData::ObjectSP object_sp = StructuredData::ParseJSON(buf);
+ StructuredData::ObjectSP object_sp =
+ StructuredData::ParseJSON(llvm::StringRef(*json).rtrim('\0'));
if (!object_sp) {
LLDB_LOGF(log, "Unable to read 'process metadata' LC_NOTE, did not "
"parse as valid JSON.");
diff --git a/lldb/test/API/macosx/lc-note/additional-registers/TestMetadataRegisters.py b/lldb/test/API/macosx/lc-note/additional-registers/TestMetadataRegisters.py
index 78046125860bf5..287a6f126c5557 100644
--- a/lldb/test/API/macosx/lc-note/additional-registers/TestMetadataRegisters.py
+++ b/lldb/test/API/macosx/lc-note/additional-registers/TestMetadataRegisters.py
@@ -2,7 +2,9 @@
import os
import re
+import struct
import subprocess
+import zlib
import lldb
from lldbsuite.test.decorators import *
@@ -13,6 +15,52 @@
class TestMetadataRegisters(TestBase):
NO_DEBUG_INFO_TESTCASE = True
+ def write_compressed_payload(self, path, json, uncompressed_size):
+ with open(path, "wb") as f:
+ # The GNU .zdebug layout: magic, big-endian uncompressed size.
+ f.write(b"ZLIB")
+ f.write(struct.pack(">Q", uncompressed_size))
+ f.write(zlib.compress(json))
+
+ def check_metadata_registers(self, corefile):
+ target = self.dbg.CreateTarget("")
+ process = target.LoadCore(corefile)
+ self.assertTrue(process.IsValid())
+ if self.TraceOn():
+ self.runCmd("bt")
+ self.runCmd("reg read -a")
+
+ thread = process.GetSelectedThread()
+ frame = thread.GetFrameAtIndex(0)
+
+ # Register sets will be
+ # from LC_THREAD:
+ # General Purpose Registers
+ # Floating Point Registers
+ # Exception State Registers
+ # from LC_NOTE metadata:
+ # Special Registers
+ self.assertEqual(frame.registers[0].GetName(), "General Purpose Registers")
+ self.assertEqual(frame.registers[3].GetName(), "Special Registers")
+
+ anotherpc = frame.registers[0].GetChildMemberWithName("anotherpc")
+ self.assertTrue(anotherpc.IsValid())
+ self.assertEqual(anotherpc.GetValueAsUnsigned(), 0x37)
+
+ jar = frame.registers[3].GetChildMemberWithName("jar")
+ self.assertTrue(jar.IsValid())
+ self.assertEqual(jar.GetValueAsUnsigned(), 10)
+ self.assertEqual(jar.GetByteSize(), 4)
+
+ bar = frame.registers[3].GetChildMemberWithName("bar")
+ self.assertTrue(bar.IsValid())
+ self.assertEqual(bar.GetByteSize(), 2)
+
+ mar = frame.registers[3].GetChildMemberWithName("mar")
+ self.assertTrue(mar.IsValid())
+ self.assertEqual(mar.GetValueAsUnsigned(), 0x10001)
+ self.assertEqual(mar.GetByteSize(), 4)
+
@requireDarwin
@skipIfRemote
def test_add_registers_via_metadata(self):
@@ -22,6 +70,10 @@ def test_add_registers_via_metadata(self):
metadata_corefile = self.getBuildArtifact("metadata.core")
bad_metadata1_corefile = self.getBuildArtifact("bad-metadata1.core")
bad_metadata2_corefile = self.getBuildArtifact("bad-metadata2.core")
+ compressed_corefile = self.getBuildArtifact("compressed.core")
+ compressed_payload = self.getBuildArtifact("compressed.payload")
+ bad_compressed_corefile = self.getBuildArtifact("bad-compressed.core")
+ bad_compressed_payload = self.getBuildArtifact("bad-compressed.payload")
add_lcnote = self.getBuildArtifact("add-lcnote")
(target, process, t, bp) = lldbutil.run_to_source_breakpoint(
@@ -38,6 +90,14 @@ def test_add_registers_via_metadata(self):
process.Kill()
target.Clear()
+ metadata = """{"threads":[{"register_info":
+ {"sets":["Special Registers", "General Purpose Registers"],
+ "registers":[
+ {"name":"jar", "value":10, "bitsize": 32, "set": 0},
+ {"name":"bar", "value":65537, "bitsize":16, "set":0},
+ {"name":"mar", "value":65537, "bitsize":32, "set":0},
+ {"name":"anotherpc", "value":55, "bitsize":64, "set": 1}]}}]}"""
+
cmd = (
add_lcnote
+ " -r"
@@ -45,17 +105,38 @@ def test_add_registers_via_metadata(self):
+ " -o '%s'" % metadata_corefile
+ " -n 'process metadata' "
+ " -s '"
- + """{"threads":[{"register_info":
- {"sets":["Special Registers", "General Purpose Registers"],
- "registers":[
- {"name":"jar", "value":10, "bitsize": 32, "set": 0},
- {"name":"bar", "value":65537, "bitsize":16, "set":0},
- {"name":"mar", "value":65537, "bitsize":32, "set":0},
- {"name":"anotherpc", "value":55, "bitsize":64, "set": 1}]}}]}"""
+ + metadata
+ "'"
)
call(cmd, shell=True)
+ self.write_compressed_payload(
+ compressed_payload, metadata.encode(), len(metadata)
+ )
+ cmd = (
+ add_lcnote
+ + " -r"
+ + " -i '%s'" % lldb_corefile
+ + " -o '%s'" % compressed_corefile
+ + " -n 'process metadata' "
+ + " -f '%s'" % compressed_payload
+ )
+ call(cmd, shell=True)
+
+ # The claimed uncompressed size exceeds what deflate can produce.
+ self.write_compressed_payload(
+ bad_compressed_payload, metadata.encode(), 1 << 62
+ )
+ cmd = (
+ add_lcnote
+ + " -r"
+ + " -i '%s'" % lldb_corefile
+ + " -o '%s'" % bad_compressed_corefile
+ + " -n 'process metadata' "
+ + " -f '%s'" % bad_compressed_payload
+ )
+ call(cmd, shell=True)
+
cmd = (
add_lcnote
+ " -r"
@@ -85,44 +166,8 @@ def test_add_registers_via_metadata(self):
)
call(cmd, shell=True)
- # Now load the corefile
- target = self.dbg.CreateTarget("")
- process = target.LoadCore(metadata_corefile)
- self.assertTrue(process.IsValid())
- if self.TraceOn():
- self.runCmd("bt")
- self.runCmd("reg read -a")
-
- thread = process.GetSelectedThread()
- frame = thread.GetFrameAtIndex(0)
-
- # Register sets will be
- # from LC_THREAD:
- # General Purpose Registers
- # Floating Point Registers
- # Exception State Registers
- # from LC_NOTE metadata:
- # Special Registers
- self.assertEqual(frame.registers[0].GetName(), "General Purpose Registers")
- self.assertEqual(frame.registers[3].GetName(), "Special Registers")
-
- anotherpc = frame.registers[0].GetChildMemberWithName("anotherpc")
- self.assertTrue(anotherpc.IsValid())
- self.assertEqual(anotherpc.GetValueAsUnsigned(), 0x37)
-
- jar = frame.registers[3].GetChildMemberWithName("jar")
- self.assertTrue(jar.IsValid())
- self.assertEqual(jar.GetValueAsUnsigned(), 10)
- self.assertEqual(jar.GetByteSize(), 4)
-
- bar = frame.registers[3].GetChildMemberWithName("bar")
- self.assertTrue(bar.IsValid())
- self.assertEqual(bar.GetByteSize(), 2)
-
- mar = frame.registers[3].GetChildMemberWithName("mar")
- self.assertTrue(mar.IsValid())
- self.assertEqual(mar.GetValueAsUnsigned(), 0x10001)
- self.assertEqual(mar.GetByteSize(), 4)
+ self.check_metadata_registers(metadata_corefile)
+ self.check_metadata_registers(compressed_corefile)
# bad1 has invalid JSON, no additional registers
target = self.dbg.CreateTarget("")
@@ -148,3 +193,12 @@ def test_add_registers_via_metadata(self):
if regset.GetName() == "a" or regset.GetName() == "b":
metadata_regset_found = True
self.assertFalse(metadata_regset_found)
+
+ # A corrupt compressed payload is rejected.
+ target = self.dbg.CreateTarget("")
+ process = target.LoadCore(bad_compressed_corefile)
+ self.assertTrue(process.IsValid())
+ frame = process.GetSelectedThread().GetFrameAtIndex(0)
+ regset_names = [regset.GetName() for regset in frame.registers]
+ self.assertIn("General Purpose Registers", regset_names)
+ self.assertNotIn("Special Registers", regset_names)
More information about the lldb-commits
mailing list