Skip to content

Bail out if we get an invalid wrapper to avoid an inifinite loop here. - #162725

Closed
AlexEne wants to merge 1 commit into
rust-lang:mainfrom
AlexEne:main
Closed

AlexEne wants to merge 1 commit into
rust-lang:mainfrom
AlexEne:main

Conversation

@AlexEne

@AlexEne AlexEne commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

If you have code like the one below and set a break-point on the first println! line with code lldb you end up getting your whole editor stuck due to the loop i just changed.

This is also related to also the problem described here: llvm/llvm-project#196812
After this PR it will show the wrong strong / weak counts for the Rc (both 0), but at least doesn't freeze the debug session.

#[test]
fn test_lldb_fail() {
    struct Holder {
        rc1: std::rc::Rc<u32>,
        rc2: std::rc::Rc<u32>,
        rc3: std::rc::Rc<u32>,
    }
    let cell: std::cell::Cell<u64> = std::cell::Cell::new(0);
    let holder = Holder {
        rc1: std::rc::Rc::new(1),
        rc2: std::rc::Rc::new(2),
        rc3: std::rc::Rc::new(3),
    };
    println!("breakpoint here: {} {}", cell.get(), holder.rc1);
    println!("Second one");
}

I reproduced it in Zed, on arch, but I suspect many editors work the same. codelldb attempts a cancellation for stuck responses, but it only cancels one so it doesn't help for the example above.

Further on, I will follow up with a codelldb PR as well to cancel checks inside the struct / field summaries, instead just the top-level variables (to further harden these cases).

This can happen with Cell<u64> and Cell<usize> not resolving correctly
in LLDB and causes hangs.
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Sep 13, 2026
@rustbot

rustbot commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

r? @Mark-Simulacrum

rustbot has assigned @Mark-Simulacrum.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @Mark-Simulacrum

@Mark-Simulacrum

Copy link
Copy Markdown
Member

@Walnut356, do you have an opinion on whether a test is worth it for this?

@Walnut356

Walnut356 commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Hmm, tbh I think unwrap_scalar_wrappers is still a bit flimsy. What it's trying to do is a bit roundabout considering we're working with integer types. I'm also not a huge fan of returning plausible (but incorrect) information without any kind of warning. SBValue can be thought of as a pointer to the debugee memory, so we should be able to just cast the weak and strong valobjs to unsigned long long rather than unwrapping. Then if those end up invalid, we can output a dummy value and print a warning.

Rc is barely tested at all atm anyway (for good reason, atm it often reads OoB memory if the allocator doesn't 0 the portion after any u8/i8 data), so we can probably skip a test for now. The plan is to have a single, much more robust test for each type we have a visualizer for. Once I get the tests running on windows and linux in CI (which shouldn't be too much longer), and wrap up gsoc stuff, it's next on the agenda.

Comment thread src/etc/lldb_providers.py
Comment on lines 1566 to 1605
def __init__(self, valobj: SBValue, _dict: LLDBOpaque, is_atomic: bool = False):
self.valobj = valobj

self.ptr = unwrap_unique_or_non_null(self.valobj.GetChildMemberWithName("ptr"))

self.value = self.ptr.GetChildMemberWithName("data" if is_atomic else "value")

self.strong = unwrap_scalar_wrappers(self.ptr.GetChildMemberWithName("strong"))
self.weak = unwrap_scalar_wrappers(self.ptr.GetChildMemberWithName("weak"))

self.value_builder = ValueBuilder(valobj)

self.update()

def num_children(self) -> int:
# Actually there are 3 children, but only the `value` should be shown as a child
return 1

def get_child_index(self, name: str) -> int:
if name == "value":
return 0
if name == "strong":
return 1
if name == "weak":
return 2
return -1

def get_child_at_index(self, index: int) -> Optional[SBValue]:
if index == 0:
return self.value
if index == 1:
return self.value_builder.from_uint("strong", self.strong_count)
if index == 2:
return self.value_builder.from_uint("weak", self.weak_count)

return None

def update(self):
self.strong_count = self.strong.GetValueAsUnsigned()
self.weak_count = self.weak.GetValueAsUnsigned() - 1

@Walnut356 Walnut356 Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __init__(self, valobj: SBValue, _dict: LLDBOpaque, is_atomic: bool = False):
self.valobj = valobj
self.ptr = unwrap_unique_or_non_null(self.valobj.GetChildMemberWithName("ptr"))
self.value = self.ptr.GetChildMemberWithName("data" if is_atomic else "value")
# infallibly gets an unsigned integer type of at least 64 bits. We don't need to worry about
# whether or not `usize` is actually smaller than that since we don't ever display the
# underlying type to the user anyway
usize_type = valobj.GetTarget().GetBasicType(eBasicTypeUnsignedLongLong)
self.strong = self.ptr.GetChildMemberWithName("strong").Cast(usize_type)
self.weak = self.ptr.GetChildMemberWithName("weak").Cast(
usize_type
)
def num_children(self) -> int:
# Actually there are 3 children, but only the `value` should be shown as a child
return 1
def get_child_index(self, name: str) -> int:
if name == "value":
return 0
if name == "strong":
return 1
if name == "weak":
return 2
return -1
def get_child_at_index(self, index: int) -> Optional[SBValue]:
if index == 0:
return self.value
if index == 1:
return self.strong
if index == 2:
return self.weak
return None
def update(self):
pass

Along the lines of this, where we only operate on SBValue objects

View changes since the review

Comment thread src/etc/lldb_providers.py
Comment on lines 1548 to 1550
strong = valobj.GetChildMemberWithName("strong").GetValueAsUnsigned()
weak = valobj.GetChildMemberWithName("weak").GetValueAsUnsigned()
return "strong={}, weak={}".format(strong, weak)

@Walnut356 Walnut356 Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
strong = valobj.GetChildMemberWithName("strong")
weak = valobj.GetChildMemberWithName("weak")
if not (strong.IsValid() and weak.IsValid()):
strong = "?"
weak = "?"
print("Unable to read ref count, see llvm/llvm-project#196812")
else:
strong = strong.GetValueAsUnsigned()
weak = weak.GetValueAsUnsigned()
return "strong={}, weak={}".format(strong, weak)

And then return some kind of dummy value and error message.

Also we can subtract 1 from weak here before printing it to the user if that's desirable. The existing Rc synthetic modifies the child itself, but that's not super necessary since it also hides that child from the user.

It's maybe not entirely necessary though because the weak count does start at 1, but iunno.

View changes since the review

@AlexEne

AlexEne commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Should we merge this first before changing more of it? I would bias towards some minimal changes to fix the infinite loop and then can iterate on the behavior? What do you think?

@Walnut356

Copy link
Copy Markdown
Contributor

Fundamentally, i think this sort of looping behavior is always going to be finicky and unreliable. Whether we patch the infinite loop then delete the loop, or just delete the loop immediately isnt a huge deal to me.

I do think we need some kind of warning and dummy value for sure though. A debugger outputting incorrect dara is worse than one that outputs nothing at all.

@AlexEne

AlexEne commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

It's not that debuggers don't output things but they get stuck making it impossible to even debug.

I will close this as it seems there is a different direction / cleanup you may want to take it and I don't have bandwidth to test and iterate on suggestions. So, I'll focus on getting the codelldb PR vadimcn/codelldb#1413 over the line instead as it's more valuable if it can defend against these type of infinite loop bugs in visualizers.

@AlexEne AlexEne closed this Sep 16, 2026
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Sep 16, 2026
jhpratt added a commit to jhpratt/rust that referenced this pull request Sep 27, 2026
[debugger visualizers] Add workaround to read `Rc` strong/weak counts

Implements a fix with the same goal as rust-lang#162725 of removing the infinite loop. Since `RcInner` is `#[repr(C)]`, we can just directly read the values from their known offsets. Worth noting that LLDB fails to even populate the fields and offsets when this bug occurs, so `#[repr(C)]` is a required invariant to retrieve the values from addresses like this:

```
(lldb) script lldb.frame.var("holder").GetNonSyntheticValue().GetChildAtIndex(0).GetChildAtIndex(0).GetChildAtIndex(0).GetType().GetPointeeType()

template<> struct RcInner<unsigned int> {
private:
    unsigned int value;
}
```

That `RcInner` should have a `strong` and `weak` field, but because LLDB can't read the type node, it omits them entirely. Thankfully, the `value` field is still read correctly and has an accurate offset.

This change has 1 small side effect, which is that the `weak` count is not decremented before it is displayed to the user. We can easily do that in the summary function, but imo it feels weird to lie about the value in memory? The `weak` count starts at 1 because the original `Rc` instance "holds" both a strong and a weak reference. It's mostly an artifact of `Rc`'s implementation, but if you're debugging, that seems like something that might matter.
jhpratt added a commit to jhpratt/rust that referenced this pull request Sep 28, 2026
[debugger visualizers] Add workaround to read `Rc` strong/weak counts

Implements a fix with the same goal as rust-lang#162725 of removing the infinite loop. Since `RcInner` is `#[repr(C)]`, we can just directly read the values from their known offsets. Worth noting that LLDB fails to even populate the fields and offsets when this bug occurs, so `#[repr(C)]` is a required invariant to retrieve the values from addresses like this:

```
(lldb) script lldb.frame.var("holder").GetNonSyntheticValue().GetChildAtIndex(0).GetChildAtIndex(0).GetChildAtIndex(0).GetType().GetPointeeType()

template<> struct RcInner<unsigned int> {
private:
    unsigned int value;
}
```

That `RcInner` should have a `strong` and `weak` field, but because LLDB can't read the type node, it omits them entirely. Thankfully, the `value` field is still read correctly and has an accurate offset.

This change has 1 small side effect, which is that the `weak` count is not decremented before it is displayed to the user. We can easily do that in the summary function, but imo it feels weird to lie about the value in memory? The `weak` count starts at 1 because the original `Rc` instance "holds" both a strong and a weak reference. It's mostly an artifact of `Rc`'s implementation, but if you're debugging, that seems like something that might matter.
rust-bors Bot pushed a commit that referenced this pull request Sep 28, 2026
Rollup merge of #163098 - Walnut356:rc_usize, r=Mark-Simulacrum

[debugger visualizers] Add workaround to read `Rc` strong/weak counts

Implements a fix with the same goal as #162725 of removing the infinite loop. Since `RcInner` is `#[repr(C)]`, we can just directly read the values from their known offsets. Worth noting that LLDB fails to even populate the fields and offsets when this bug occurs, so `#[repr(C)]` is a required invariant to retrieve the values from addresses like this:

```
(lldb) script lldb.frame.var("holder").GetNonSyntheticValue().GetChildAtIndex(0).GetChildAtIndex(0).GetChildAtIndex(0).GetType().GetPointeeType()

template<> struct RcInner<unsigned int> {
private:
    unsigned int value;
}
```

That `RcInner` should have a `strong` and `weak` field, but because LLDB can't read the type node, it omits them entirely. Thankfully, the `value` field is still read correctly and has an accurate offset.

This change has 1 small side effect, which is that the `weak` count is not decremented before it is displayed to the user. We can easily do that in the summary function, but imo it feels weird to lie about the value in memory? The `weak` count starts at 1 because the original `Rc` instance "holds" both a strong and a weak reference. It's mostly an artifact of `Rc`'s implementation, but if you're debugging, that seems like something that might matter.
github-actions Bot pushed a commit to rust-lang/rust-analyzer that referenced this pull request Oct 1, 2026
[debugger visualizers] Add workaround to read `Rc` strong/weak counts

Implements a fix with the same goal as rust-lang/rust#162725 of removing the infinite loop. Since `RcInner` is `#[repr(C)]`, we can just directly read the values from their known offsets. Worth noting that LLDB fails to even populate the fields and offsets when this bug occurs, so `#[repr(C)]` is a required invariant to retrieve the values from addresses like this:

```
(lldb) script lldb.frame.var("holder").GetNonSyntheticValue().GetChildAtIndex(0).GetChildAtIndex(0).GetChildAtIndex(0).GetType().GetPointeeType()

template<> struct RcInner<unsigned int> {
private:
    unsigned int value;
}
```

That `RcInner` should have a `strong` and `weak` field, but because LLDB can't read the type node, it omits them entirely. Thankfully, the `value` field is still read correctly and has an accurate offset.

This change has 1 small side effect, which is that the `weak` count is not decremented before it is displayed to the user. We can easily do that in the summary function, but imo it feels weird to lie about the value in memory? The `weak` count starts at 1 because the original `Rc` instance "holds" both a strong and a weak reference. It's mostly an artifact of `Rc`'s implementation, but if you're debugging, that seems like something that might matter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants