Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add misc debugging helpers and feature to poison dead memory #1241

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,6 @@ sticky_immix_non_moving_nursery = []
immix_stress_copying = []
# Reduce block size for ImmixSpace. This mitigates fragmentation when defrag is disabled.
immix_smaller_block = []
# Zero the unmarked lines after a GC cycle in immix. This helps debug untraced objects.
immix_zero_on_release = []

# Run sanity GC
sanity = []
Expand All @@ -160,6 +158,9 @@ nogc_no_zeroing = ["nogc_lock_free"]
# Q: Why do we need this as a compile time flat? We can always set the number of GC threads through options.
single_worker = []

# Poison dead objects on release. Each space uses a different poison value. This helps debug untraced objects.
poison_on_release = []

# To run expensive comprehensive runtime checks, such as checking duplicate edges
extreme_assertions = []

Expand Down
5 changes: 5 additions & 0 deletions src/policy/copyspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ impl<VM: VMBinding> CopySpace<VM> {
}

pub fn release(&self) {
#[cfg(feature = "poison_on_release")]
for (start, size) in self.pr.iterate_allocated_regions() {
crate::util::memory::set(start, 0xbc, size);
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using an arbitrary value, you can define a mapping between space and its poison value, or use something from the space (such as the SpaceDescriptor index, or the space name).

I would suggest creating a module crate::util::poison_on_release. Define the poison values there, and define functions to poison a memory region.

Copy link
Member

Choose a reason for hiding this comment

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

I am not sure if we need the different values to tell which space it is, especially after Kunshan's change in #1236. But if we do want to use different values, there needs to be a place where we can look at and know what the value means.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well that might be true. It was helpful to me but that predates when I was using prctl. I can remove it and replace it with a static pattern of 0xab or something obviously nonsense.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The annotation of #1236 is precise for Map64, but imprecise for Map32 due to chunk reuse between spaces. It is still helpful to let each space have a different poison value.

}

for (start, size) in self.pr.iterate_allocated_regions() {
// Clear the forwarding bits if it is on the side.
if let MetadataSpec::OnSide(side_forwarding_status_table) =
Expand Down
9 changes: 7 additions & 2 deletions src/policy/immix/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,13 @@ impl Block {
holes += 1;
}

#[cfg(feature = "immix_zero_on_release")]
crate::util::memory::zero(line.start(), Line::BYTES);
// Cheeky trick to encode the released line into the poisoned value
#[cfg(feature = "poison_on_release")]
crate::util::memory::set_pattern(
line.start(),
(0xdeadbeef ^ line.start().as_usize()) & !0xff,
Copy link
Member

Choose a reason for hiding this comment

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

Why poison as a pattern rather than a u8 value as other spaces? What is the pattern here? How do we supposed to use the pattern?

Copy link
Member

Choose a reason for hiding this comment

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

This removes the existing code for immix_zero_on_release. So immix will not zero lines even if immix_zero_on_release is enabled.

You should have an assertion to make sure that immix_zero_on_release and poison_on_release should not co-exist, as they conflict with each other for Immix space, and you should zero/poison the memory based on which feature is enabled.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This feature supersedes zero on release. The pattern here is helpful since it encodes the line that was released into the pattern. So you can XOR with 0xdeadbeef to get the exact line that was released and where the object comes from. It was useful to me while debugging ART.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

immix_zero_on_release was a debugging feature to catch untraced objects. But using 0x0 can actually make it harder to debug (in my and others' experience) since you don't know if it's a null pointer or something that was released early. So previously some of the students and I used an obvious nonsense value like 0xab as the pattern. This is refinement of the same since it encodes the line into the pattern.

Copy link
Member

@qinsoon qinsoon Nov 29, 2024

Choose a reason for hiding this comment

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

If you think "poison_on_release" is better than "zero_on_release", just remove the immix_zero_on_release feature, and make sure that poison_on_release can be a replacement for immix_zero_on_release. With the current code, immix_zero_on_release is broken.

Copy link
Member

Choose a reason for hiding this comment

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

So you can XOR with 0xdeadbeef to get the exact line that was released and where the object comes from.

I am not following here. Can you get the line by aligning down from the faulty address? Why is XOR needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The problem isn't the line itself but what object was referring to it. In my case I did not have a debugger available because crashes were happening at Android boot time. So I had to be creative to debug memory corruption errors (hence the dump_ram_around_address function as well).

So consider the case that a line has been overwritten when an object inside it was live.

Object = 0x12000040
Line = 0x12000000
Encoded pattern = 0xCCADBE00

An object referring to 0x12000040 would maybe load a field at an offset 0x8. So it loads the address 0x12000048. If we wrote just 0x0s into the line then we would get a SIGSEGV but then no way to figure out what address was loaded. But if we wrote 0xCCADBE00 to it then the SIGSEGV output would list say 0xCCADBE00 and then we can XOR with 0xDEADBE00 and get back 0x12000000 which was the line that was overwritten.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is I guess unnecessary in the general case if you have a debugger, but I definitely found it useful.

Copy link
Member

Choose a reason for hiding this comment

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

I see. Without the explanation above, I wouldn't know why you encode the value in that way, and how I am supposed to use the encoded value. Those explanation should go into the comments and the docs in the code so others won't have the same question as I did when they encounter the code and when they use this feature.

Besides, I suggest separating it into a different features:

  • poison_on_release: this just generally poisons released memory (or dead objects) with an identifiable value.
  • immix_poison_with_line_address_on_release (or something like this): it poisons the memory with an encoded line address.

Line::BYTES,
);

// We need to clear the pin bit if it is on the side, as this line can be reused
#[cfg(feature = "object_pinning")]
Expand Down
6 changes: 4 additions & 2 deletions src/policy/largeobjectspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,10 @@ impl<VM: VMBinding> LargeObjectSpace<VM> {
let sweep = |object: ObjectReference| {
#[cfg(feature = "vo_bit")]
crate::util::metadata::vo_bit::unset_vo_bit(object);
self.pr
.release_pages(get_super_page(object.to_object_start::<VM>()));
let start = object.to_object_start::<VM>();
#[cfg(feature = "poison_on_release")]
crate::util::memory::set(start, 0xed, VM::VMObjectModel::get_current_size(object));
self.pr.release_pages(get_super_page(start));
};
if sweep_nursery {
for object in self.treadmill.collect_nursery() {
Expand Down
22 changes: 22 additions & 0 deletions src/scheduler/gc_work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ impl<E: ProcessEdgesWork> ObjectTracer for ProcessEdgesWorkTracer<E> {
/// Forward the `trace_object` call to the underlying `ProcessEdgesWork`,
/// and flush as soon as the underlying buffer of `process_edges_work` is full.
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
debug_assert!(
<E::VM as VMBinding>::VMObjectModel::is_object_sane(object),
"Object {:?} is not sane!",
object,
);
let result = self.process_edges_work.trace_object(object);
self.flush_if_full();
result
Expand Down Expand Up @@ -693,6 +698,12 @@ impl<VM: VMBinding> ProcessEdgesWork for SFTProcessEdges<VM> {
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
use crate::policy::sft::GCWorkerMutRef;

debug_assert!(
<VM as VMBinding>::VMObjectModel::is_object_sane(object),
"Object {:?} is not sane!",
object,
);

// Erase <VM> type parameter
let worker = GCWorkerMutRef::new(self.worker());

Expand Down Expand Up @@ -840,6 +851,11 @@ pub trait ScanObjectsWork<VM: VMBinding>: GCWork<VM> + Sized {

if <VM as VMBinding>::VMScanning::support_slot_enqueuing(tls, object) {
trace!("Scan object (slot) {}", object);
debug_assert!(
<VM as VMBinding>::VMObjectModel::is_object_sane(object),
"Object {:?} is not sane!",
object,
);
// If an object supports slot-enqueuing, we enqueue its slots.
<VM as VMBinding>::VMScanning::scan_object(tls, object, &mut closure);
self.post_scan_object(object);
Expand Down Expand Up @@ -977,6 +993,12 @@ impl<VM: VMBinding, P: PlanTraceObject<VM> + Plan<VM = VM>, const KIND: TraceKin
// Skip slots that are not holding an object reference.
return;
};
debug_assert!(
<VM as VMBinding>::VMObjectModel::is_object_sane(object),
"Object {:?} from slot {:?} is not sane!",
object,
slot,
);
let new_object = self.trace_object(object);
if P::may_move_objects::<KIND>() && new_object != object {
slot.store(new_object);
Expand Down
42 changes: 42 additions & 0 deletions src/util/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,48 @@ pub fn set(start: Address, val: u8, len: usize) {
}
}

/// Set a range of memory to the given pattern. Similar to C++'s std::fill.
pub fn set_pattern(start: Address, pattern: usize, len: usize) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suggest using a fixed-size integer type (or a type trait) for the pattern. At the call site, we give the pattern as a hexadecimal literal, but here we write memory at the granularity of usize which is platform-dependent. For example, if pattern is 0xdeadbeef, and the platform is x86_64, then the memory will be filled with EF BE AD DE 00 00 00 00 EF BE AD DE 00 00 00 00 EF BE ..., and I think we don't want the 00 00 00 00 in between.

Suggested change
pub fn set_pattern(start: Address, pattern: usize, len: usize) {
pub fn set_pattern(start: Address, pattern: u32, num_bytes: usize) {

Alternatively

Suggested change
pub fn set_pattern(start: Address, pattern: usize, len: usize) {
trait UnsignedIntButNotUsize: num_traits::PrimInt + num_traits::Unsigned + num_traits::ToBytes {}
impl UnsignedIntButNotUsize for u8 {}
impl UnsignedIntButNotUsize for u16 {}
impl UnsignedIntButNotUsize for u32 {}
impl UnsignedIntButNotUsize for u64 {}
pub fn set_pattern<T: UnsignedIntButNotUsize>(start: Address, pattern: T, num_bytes: usize) {

debug_assert!(std::mem::size_of::<usize>() <= len);
debug_assert!(len % std::mem::size_of::<usize>() == 0);

let end_addr = (start + len).to_mut_ptr::<usize>();
let mut current = start.to_mut_ptr::<usize>();
while current < end_addr {
unsafe {
*current = pattern;
current = current.add(1);
}
}
}

/// Dump RAM around a given address. Note that be careful when using this function as it may
/// segfault for unmapped memory. ONLY use it for locations that are KNOWN to be broken AND
/// allocated by MMTk.
///
/// # Safety
/// Extremely unsafe for arbitrary addresses since they may not have been mapped. Only use
/// this function for locations that are KNOWN to be broken AND allocated by MMTk.
pub unsafe fn dump_ram_around_address(addr: Address, bytes: usize) -> String {
let mut string: String = String::new();
let end_addr = (addr + bytes).to_ptr::<usize>();
let mut current = (addr - bytes).to_ptr::<usize>();
while current < end_addr {
if current == addr.to_ptr::<usize>() {
string.push_str(" | ");
} else {
string.push(' ');
}
let s = current.read();
#[cfg(target_pointer_width = "64")]
string.push_str(format!("{:#018x}", s).as_str());
#[cfg(target_pointer_width = "32")]
string.push_str(format!("{:#010x}", s).as_str());
current = current.add(1);
}
string
}

/// Demand-zero mmap:
/// This function mmaps the memory and guarantees to zero all mapped memory.
/// This function WILL overwrite existing memory mapping. The user of this function
Expand Down
Loading