[LLDB] Hide unresolvable children from ObjC tagged pointers (#211936)

Foundation's small value inline representations have valid summaries and
sometimed synthetic children, but LLDB cannot access any ivars from
their base classes (such as [NSObject isa]).

Use a synthetic child provider to hide them.

rdar://182434208

Assisted-by: claude
This commit is contained in:
Adrian Prantl 2026-07-27 14:59:17 -07:00 committed by GitHub
parent 8a49f8839c
commit 4f02e1e45c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 231 additions and 0 deletions

View file

@ -881,6 +881,45 @@ lldb::TypeCategoryImplSP ObjCLanguage::GetFormatters() {
return g_category;
}
HardcodedFormatters::HardcodedSyntheticFinder
ObjCLanguage::GetHardcodedSynthetics() {
static llvm::once_flag g_initialize;
static HardcodedFormatters::HardcodedSyntheticFinder g_formatters;
llvm::call_once(g_initialize, []() -> void {
// An Objective-C tagged pointer (e.g. a single-index NSIndexSet
// or a small NSNumber) packs its whole state into the pointer
// bits and has no object in memory. Since its declared base
// classes and ivars cannot be read, this synthtic child provider
// hides them. Other synthetic child providers take precedence of
// this.
g_formatters.push_back(
[](lldb_private::ValueObject &valobj, lldb::DynamicValueType,
FormatManager &) -> SyntheticChildren::SharedPointer {
static CXXSyntheticChildren::SharedPointer formatter_sp(
new CXXSyntheticChildren(
SyntheticChildren::Flags()
.SetCascades(true)
.SetSkipPointers(false)
.SetSkipReferences(false)
.SetNonCacheable(true),
"tagged pointer synthetic children",
lldb_private::formatters::ObjCClassSyntheticFrontEndCreator));
ProcessSP process_sp = valobj.GetProcessSP();
if (!process_sp)
return nullptr;
if (ObjCLanguageRuntime *runtime =
ObjCLanguageRuntime::Get(*process_sp))
if (runtime->IsTaggedPointerValue(valobj))
return formatter_sp;
return nullptr;
});
});
return g_formatters;
}
std::vector<FormattersMatchCandidate>
ObjCLanguage::GetPossibleFormattersMatches(ValueObject &valobj,
lldb::DynamicValueType use_dynamic) {

View file

@ -146,6 +146,9 @@ public:
lldb::TypeCategoryImplSP GetFormatters() override;
HardcodedFormatters::HardcodedSyntheticFinder
GetHardcodedSynthetics() override;
std::vector<FormattersMatchCandidate>
GetPossibleFormattersMatches(ValueObject &valobj,
lldb::DynamicValueType use_dynamic) override;

View file

@ -290,6 +290,23 @@ ObjCLanguageRuntime::GetClassDescriptor(ValueObject &valobj) {
return objc_class_sp;
}
bool ObjCLanguageRuntime::IsTaggedPointerValue(ValueObject &in_value) {
TaggedPointerVendor *tagged_pointer_vendor = GetTaggedPointerVendor();
if (!tagged_pointer_vendor)
return false;
// Only Objective-C object values can be tagged pointers.
if (!(in_value.GetTypeInfo() & lldb::eTypeIsObjC))
return false;
addr_t ptr = in_value.IsPointerType() ? in_value.GetPointerValue().address
: in_value.GetAddressOf().address;
if (ptr == LLDB_INVALID_ADDRESS)
return false;
return tagged_pointer_vendor->IsPossibleTaggedPointer(ptr);
}
ObjCLanguageRuntime::ClassDescriptorSP
ObjCLanguageRuntime::GetNonKVOClassDescriptor(ValueObject &valobj) {
ObjCLanguageRuntime::ClassDescriptorSP objc_class_sp(

View file

@ -233,6 +233,8 @@ public:
virtual TaggedPointerVendor *GetTaggedPointerVendor() { return nullptr; }
bool IsTaggedPointerValue(ValueObject &in_value);
typedef std::shared_ptr<EncodingToType> EncodingToTypeSP;
virtual EncodingToTypeSP GetEncodingToType();

View file

@ -0,0 +1,4 @@
OBJC_SOURCES := main.m
LD_EXTRAS := -lobjc -framework Foundation
include Makefile.rules

View file

@ -0,0 +1,115 @@
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class TaggedPointerChildrenTestCase(TestBase):
def is_tagged(self, valobj):
"""Return True if valobj holds an Objective-C tagged pointer."""
res = lldb.SBCommandReturnObject()
ci = self.dbg.GetCommandInterpreter()
ci.HandleCommand(
"language objc tagged-pointer info %s" % valobj.GetValue(), res
)
return res.Succeeded() and "is tagged" in res.GetOutput()
def assert_no_unreadable_children(self, valobj):
"""Recursively assert no child of valobj fails with a memory-read error."""
for i in range(valobj.GetNumChildren()):
child = valobj.GetChildAtIndex(i)
err = child.GetError()
self.assertFalse(
err.Fail() and "read memory" in (err.GetCString() or ""),
"child '%s' of '%s' has a memory-read error: %s"
% (child.GetName(), valobj.GetName(), err.GetCString()),
)
self.assert_no_unreadable_children(child)
@skipUnlessDarwin
@skipIf(archs=["i386", "i686"])
def test(self):
"""
Test that Objective-C tagged pointers (inline values) do not present phantom,
memory-backed children.
"""
self.build()
_, _, thread, _ = lldbutil.run_to_source_breakpoint(
self, "// break here", lldb.SBFileSpec("main.m")
)
frame = thread.GetSelectedFrame()
tagged = frame.FindVariable("tagged")
self.assertTrue(tagged.IsValid(), "found 'tagged'")
# If this platform does not represent a single-index NSIndexSet as a
# tagged pointer there is nothing to test.
if not self.is_tagged(tagged):
self.skipTest("NSIndexSet is not a tagged pointer on this platform")
# The tagged pointer is still summarized.
self.assertIsNotNone(tagged.GetSummary())
self.assertIn("index", tagged.GetSummary())
# But it must not present any unresolvable children.
self.assertEqual(
tagged.GetNumChildren(),
0,
"tagged pointer must not have children",
)
self.assertFalse(tagged.MightHaveChildren())
self.assert_no_unreadable_children(tagged)
# Sanity check.
heap = frame.FindVariable("heap")
self.assertTrue(heap.IsValid(), "found 'heap'")
self.assertFalse(self.is_tagged(heap), "'heap' is a real object")
self.assertGreater(heap.GetNumChildren(), 0, "real object still has children")
self.assert_no_unreadable_children(heap)
@skipUnlessDarwin
@skipIf(archs=["i386", "i686"])
def test_registered_synthetic_takes_precedence(self):
"""
A synthetic child provider registered for the type must still fire for
a tagged pointer. ObjCLanguage's hardcoded "no children" provider is
only a fallback and must be consulted after any registered synthetic.
"""
self.build()
_, _, thread, _ = lldbutil.run_to_source_breakpoint(
self, "// break here", lldb.SBFileSpec("main.m")
)
frame = thread.GetSelectedFrame()
tagged = frame.FindVariable("tagged")
self.assertTrue(tagged.IsValid(), "found 'tagged'")
if not self.is_tagged(tagged):
self.skipTest("NSIndexSet is not a tagged pointer on this platform")
# Without a registered synthetic, the hardcoded fallback reports no
# children for the tagged pointer.
self.assertEqual(tagged.GetNumChildren(), 0)
# Register a synthetic child provider for NSIndexSet.
import os
provider = os.path.join(os.path.dirname(__file__), "tagged_synth_provider.py")
self.runCmd("command script import " + provider)
self.runCmd(
'type synthetic add -x "NSIndexSet" '
"-l tagged_synth_provider.TaggedSyntheticProvider"
)
self.addTearDownHook(lambda: self.runCmd("type synthetic clear", check=False))
# The registered provider must now win over the hardcoded fallback.
tagged = frame.FindVariable("tagged")
self.assertEqual(
tagged.GetNumChildren(),
1,
"registered synthetic must take precedence over the hardcoded "
"tagged-pointer fallback",
)
self.assertTrue(tagged.MightHaveChildren())
child = tagged.GetChildAtIndex(0)
self.assertEqual(child.GetName(), "synthetic_child")
self.assertEqual(child.GetValueAsSigned(), 42)

View file

@ -0,0 +1,19 @@
#import <Foundation/Foundation.h>
int main(int argc, const char *argv[]) {
@autoreleasepool {
// A single-index NSIndexSet is stored as an Objective-C tagged pointer.
// It has a summary but no ivars or base classes LLDB could materialize
// from memory.
NSIndexSet *tagged = [NSIndexSet indexSetWithIndex:1];
// A discontiguous set cannot be tagged, so it is a real heap object with a
// readable isa and ivar layout.
NSMutableIndexSet *heap = [NSMutableIndexSet indexSet];
[heap addIndex:1];
[heap addIndex:1000000];
NSLog(@"%@ %@", tagged, heap); // break here
}
return 0;
}

View file

@ -0,0 +1,32 @@
import lldb
class TaggedSyntheticProvider:
"""A custom synthetic child provider that produces a single child that
does not depend on reading the tagged pointer's (nonexistent) backing
memory, so it works for tagged pointers.
"""
def __init__(self, valobj, internal_dict):
self.valobj = valobj
def num_children(self):
return 1
def get_child_index(self, name):
return 0 if name == "synthetic_child" else -1
def get_child_at_index(self, index):
if index != 0:
return lldb.SBValue()
# Build the child from raw data rather than by reading the object, so it
# works for a tagged pointer (which has no backing memory).
target = self.valobj.GetTarget()
int_type = target.GetBasicType(lldb.eBasicTypeInt)
data = lldb.SBData.CreateDataFromSInt32Array(
target.GetByteOrder(), target.GetAddressByteSize(), [42]
)
return self.valobj.CreateValueFromData("synthetic_child", data, int_type)
def update(self):
return False