-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
A debug allocator which removes overalignment from align < 8 allocations #99074
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,6 +127,128 @@ pub use alloc_crate::alloc::*; | |
#[derive(Debug, Default, Copy, Clone)] | ||
pub struct System; | ||
|
||
use crate::sys::alloc::System as Imp; | ||
|
||
// When debug assertions are not enabled, `System` just forwards down to the particular platform | ||
// implementation. | ||
#[cfg(not(debug_assertions))] | ||
#[stable(feature = "alloc_system_type", since = "1.28.0")] | ||
unsafe impl GlobalAlloc for System { | ||
#[inline] | ||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 { | ||
unsafe { Imp.alloc(layout) } | ||
} | ||
|
||
#[inline] | ||
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { | ||
unsafe { Imp.alloc_zeroed(layout) } | ||
} | ||
|
||
#[inline] | ||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { | ||
unsafe { Imp.dealloc(ptr, layout) } | ||
} | ||
|
||
#[inline] | ||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { | ||
unsafe { Imp.realloc(ptr, layout, new_size) } | ||
} | ||
} | ||
|
||
// Some system allocators (most notably any provided by calling malloc) will always return pointers | ||
// with an alignment of 8. So for any allocation with an alignment less than 8, we increase the | ||
// alignment to 8 and return a pointer which is offset into the allocation such that it is not | ||
// over-aligned. | ||
// We always bump up the size of an allocation by 8 when the alignment is less than 8. | ||
#[cfg(debug_assertions)] | ||
trait LayoutExt { | ||
fn with_alignment_padding(self) -> Self; | ||
unsafe fn add_alignment_padding(self, ptr: *mut u8) -> *mut u8; | ||
unsafe fn remove_alignment_padding(self, ptr: *mut u8) -> *mut u8; | ||
} | ||
#[cfg(debug_assertions)] | ||
impl LayoutExt for Layout { | ||
fn with_alignment_padding(self) -> Self { | ||
if self.align() < 8 { | ||
Layout::from_size_align(self.size() + (8 - self.align()), 8).unwrap() | ||
} else { | ||
self | ||
} | ||
} | ||
|
||
unsafe fn add_alignment_padding(self, ptr: *mut u8) -> *mut u8 { | ||
if !ptr.is_null() && self.align() < 8 { | ||
// SAFETY: This must be called on a pointer previously returned by a padded Layout, | ||
// which will always have space to do this offset | ||
unsafe { ptr.add(8 - self.align()) } | ||
} else { | ||
ptr | ||
} | ||
} | ||
|
||
unsafe fn remove_alignment_padding(self, ptr: *mut u8) -> *mut u8 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should/can we check to ensure the input pointer has the lowest bits set according to the align, and abort if not (because that means they gave us the wrong alignment, which happens). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you referring to detecting an attempt to In this PR I'm more interested to know if t-libs is willing to take such an allocator, and I'm starting with a behavior that I know exposes a lot of latent bugs in the wild. We can always add more things to this later if it is accepted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I mean if you alloc with align 2, then try and dealloc with align 1, the offset calculated will be wrong, wouldn't it? Because the offset is calculated based on the alignment. Recovering the alignment from the given to As in, the address we were given was the same as the address we passed back from alloc, but the alignment is different. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahhhhh I see. Yes the current implementation is definitely not good here, because it turns code which is UB but works into an error that only makes any sense if you know about this implementation. I think eventually we should detect and report that error, but for this PR I am wary of scope creep and I'm not even sure how to print in an allocator. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you could allocate a bit more space to store some information in the bytes directly above the pointer you hand back to to the user (either the real Layout, the originally allocated pointer, etc. There are multiple approaches that could be taken). Then you can extract it with read_unaligned. This would also make it easier to add future checks that use this information to verify other things, such as the layout being correct. I think it might be slightly clearer than the bitwise trickery in this PR, and it's pretty typical of an allocator to have information in some sort of "header" anyway. |
||
// We cannot just do the inverse of add_alignment_padding, because if a user deallocates | ||
// with the wrong Layout, we would use that wrong Layout here to deduce the wrong offset to | ||
// remove from the pointer. That would turn code that works fine because the underlying | ||
// allocator ignores the Layout (but is technically UB) into code which does worse UB or | ||
// halts the program with an unhelpful diagnostic from the underlying allocator. | ||
// So we have two reasonable options. We could detect and clearly report the error | ||
// ourselves, or since we know that all our alignment adjustments involve the low 3 bits, | ||
// we could clear those and make this allocator transparent. | ||
// At the moment we do the latter because it is unclear how to emit an error message from | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO this really needs to report the issue/abort/something. What it does now seems to just be returning a misaligned pointer silently, and hoping the caller notices. This seems likely to end up not noticed at all, or noticed far away from the allocation site. I don't have strong thoughts on how to handle it, but I think printing output to stderr and aborting might be a start (or perhaps seeing what Given that this is mostly the scaffolding for other checks... I think it's worth figuring this out sooner rather than later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An issue is that
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The default allocator is in I didn't mean actually use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also to be clear, even in And it's a debugging feature, one that is only available via unstable flags. It not being possible on some targets doesn't mean it can't be available on any. We can always just not enable this on targets where we'd have no method of doing anything about detected misuse. That said, I think just about everything with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure this doesn't return a misaligned pointer silently. The effect of this strategy is to populate the lowest 3 bits to the greatest extent possible on the application side, and on the actual system allocator side keep them always clear. So this just indiscriminately clears the lowest 3 bits when handing a pointer back to the system allocator, which I'm pretty sure makes deallocation with the wrong layout totally fine if the underlying system allocator also doesn't care about layout. |
||
// inside an allocator. | ||
const ALIGNMENT_MASK: usize = usize::MAX << 3; | ||
ptr.map_addr(|addr| addr & ALIGNMENT_MASK) | ||
} | ||
} | ||
|
||
// When debug assertions are enabled, we wrap the platform allocator with extra logic to help | ||
// expose bugs. | ||
#[cfg(debug_assertions)] | ||
#[stable(feature = "alloc_system_type", since = "1.28.0")] | ||
unsafe impl GlobalAlloc for System { | ||
#[inline] | ||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 { | ||
if layout.size() > isize::MAX as usize - 8 { | ||
return ptr::null_mut(); | ||
} | ||
unsafe { | ||
let ptr = Imp.alloc(layout.with_alignment_padding()); | ||
layout.add_alignment_padding(ptr) | ||
} | ||
} | ||
|
||
#[inline] | ||
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { | ||
unsafe { | ||
let ptr = Imp.alloc_zeroed(layout.with_alignment_padding()); | ||
layout.add_alignment_padding(ptr) | ||
} | ||
} | ||
|
||
#[inline] | ||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { | ||
unsafe { | ||
let ptr = layout.remove_alignment_padding(ptr); | ||
Imp.dealloc(ptr, layout.with_alignment_padding()) | ||
} | ||
} | ||
|
||
#[inline] | ||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { | ||
if new_size > isize::MAX as usize - 8 { | ||
return ptr::null_mut(); | ||
} | ||
unsafe { | ||
let ptr = layout.remove_alignment_padding(ptr); | ||
let new_layout = | ||
Layout::from_size_align(new_size, layout.align()).unwrap().with_alignment_padding(); | ||
let ptr = Imp.realloc(ptr, layout.with_alignment_padding(), new_layout.size()); | ||
layout.add_alignment_padding(ptr) | ||
} | ||
} | ||
} | ||
|
||
impl System { | ||
#[inline] | ||
fn alloc_impl(&self, layout: Layout, zeroed: bool) -> Result<NonNull<[u8]>, AllocError> { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do want to say that I think this is the right place to insert such checks, since it neatly sidesteps questions about what programs which override the global allocator get to rely on. Thanks!