This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 9a8400ea86 fix(arrow-buffer): prevent use-after-free in
Buffer::shrink_to_fit when MemoryReservation::resize panics (#10932)
9a8400ea86 is described below
commit 9a8400ea86447d2b83a348285b5554170c7a830c
Author: RIchard Baah <[email protected]>
AuthorDate: Thu Sep 3 07:27:10 2026 -0400
fix(arrow-buffer): prevent use-after-free in Buffer::shrink_to_fit when
MemoryReservation::resize panics (#10932)
# Which issue does this PR close?
<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax.
-->
- Closes #10379.
# Rationale for this change
Buffer maintains two pointers that must always agree: `Buffer::ptr` (the
raw *const u8 into the allocation) and `Bytes::ptr` (the NonNull<u8>
that owns the allocation). Before this fix, `shrink_to_fit` only updated
`Buffer::ptr` if `try_realloc` returned Ok. But `try_realloc` can panic;
specifically inside `MemoryReservation::resize`, a user-supplied
callback after the realloc syscall already succeeded and `Bytes::ptr`
was updated. That left the two pointers out of sync: `Bytes::ptr`
pointed to the new (valid) allocation, `Buffer::ptr` still pointed to
the old (freed) allocation. Any caller that used `catch_unwind` to
recover from the panic could then call `buf.as_slice()` and read freed
memory.
<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->
# What changes are included in this PR?
replaced the conditional if try_realloc(...).is_ok() { update ptr } with
a PtrSync drop guard that unconditionally syncs `Buffer::ptr` to
`Bytes::ptr` + offset when it drops. Since Rust runs destructors during
panic unwinding, the guard fires before the panic propagates.
<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->
# Are these changes tested?
yes. a similar test to what was used in #10379
<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
3. Serve as another way to document the expected behavior of the code
If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
If this PR claims a performance improvement, please include evidence
such as benchmark results.
-->
# Are there any user-facing changes?
no
<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.
If there are any breaking changes to public APIs, please call them out.
-->
---
arrow-buffer/src/buffer/immutable.rs | 75 ++++++++++++++++++++++++++++++------
arrow-buffer/src/bytes.rs | 11 +++++-
2 files changed, 74 insertions(+), 12 deletions(-)
diff --git a/arrow-buffer/src/buffer/immutable.rs
b/arrow-buffer/src/buffer/immutable.rs
index c5d026db8d..5d2a1f80f4 100644
--- a/arrow-buffer/src/buffer/immutable.rs
+++ b/arrow-buffer/src/buffer/immutable.rs
@@ -224,17 +224,16 @@ impl Buffer {
if desired_capacity < self.capacity()
&& let Some(bytes) = Arc::get_mut(&mut self.data)
{
- if bytes.try_realloc(desired_capacity).is_ok() {
- // Realloc complete - update our pointer into `bytes`:
- self.ptr = if is_empty {
- bytes.as_ptr()
- } else {
- // SAFETY: we kept all elements leading up to the offset
- unsafe { bytes.as_ptr().add(offset) }
- }
- } else {
- // Failure to reallocate is fine; we just failed to free up
memory.
- }
+ bytes
+ .try_realloc(desired_capacity, |base| {
+ self.ptr = if is_empty {
+ base.as_ptr()
+ } else {
+ // SAFETY: we kept all elements leading up to the
offset
+ unsafe { base.as_ptr().add(offset) }
+ };
+ })
+ .ok(); // Failure to reallocate is fine; we just failed to
free up memory.
}
}
@@ -1158,4 +1157,58 @@ mod tests {
assert_eq!(buffer_back.as_slice(), expected.as_slice());
}
}
+
+ #[test]
+ #[cfg(feature = "pool")]
+ fn test_shrink_to_fit_panicking_reservation() {
+ use std::panic::{AssertUnwindSafe, catch_unwind};
+
+ use crate::pool::{MemoryPool, MemoryReservation};
+
+ #[derive(Debug)]
+ struct PanicPool;
+
+ #[derive(Debug)]
+ struct PanicReservation {
+ panicked: bool,
+ }
+
+ impl MemoryReservation for PanicReservation {
+ fn size(&self) -> usize {
+ 0
+ }
+ fn resize(&mut self, _: usize) {
+ if !self.panicked {
+ self.panicked = true;
+ panic!("intentional panic in resize");
+ }
+ }
+ }
+
+ impl MemoryPool for PanicPool {
+ fn reserve(&self, _: usize) -> Box<dyn MemoryReservation> {
+ Box::new(PanicReservation { panicked: false })
+ }
+ fn available(&self) -> isize {
+ isize::MAX
+ }
+ fn used(&self) -> usize {
+ 0
+ }
+ fn capacity(&self) -> usize {
+ usize::MAX
+ }
+ }
+
+ let pool = PanicPool;
+ let data: Vec<u8> = (0..8).collect();
+ let mut buf = Buffer::from_slice_ref(data.as_slice());
+ buf.claim(&pool);
+
+ // shrink_to_fit panics because PanicReservation::resize panics, but
+ // Buffer::ptr must stay consistent with Bytes::ptr (no
use-after-free).
+ let _ = catch_unwind(AssertUnwindSafe(|| buf.shrink_to_fit()));
+
+ assert_eq!(buf.as_slice(), data.as_slice());
+ }
}
diff --git a/arrow-buffer/src/bytes.rs b/arrow-buffer/src/bytes.rs
index ef3ede7e43..4db48998ac 100644
--- a/arrow-buffer/src/bytes.rs
+++ b/arrow-buffer/src/bytes.rs
@@ -124,7 +124,15 @@ impl Bytes {
/// Returns `Err` if the memory was allocated with a custom allocator,
/// or the call to `realloc` failed, for whatever reason.
/// In case of `Err`, the [`Bytes`] will remain as it was (i.e. have the
old size).
- pub(crate) fn try_realloc(&mut self, new_len: usize) -> Result<(), ()> {
+ ///
+ /// `on_reallocated` is called after [`Bytes`] has updated its internal
+ /// pointer, but before resizing the memory reservation, which may call
user
+ /// code.
+ pub(crate) fn try_realloc(
+ &mut self,
+ new_len: usize,
+ on_reallocated: impl FnOnce(NonNull<u8>),
+ ) -> Result<(), ()> {
if let Deallocation::Standard(old_layout) = self.deallocation {
if old_layout.size() == new_len {
return Ok(()); // Nothing to do
@@ -152,6 +160,7 @@ impl Bytes {
self.ptr = ptr;
self.len = new_len;
self.deallocation = Deallocation::Standard(new_layout);
+ on_reallocated(ptr);
#[cfg(feature = "pool")]
{