Skip to content

unwind safety fixes#140

Open
Mirko-A wants to merge 3 commits intoRust-for-Linux:mainfrom
Mirko-A:mirko/fix-unwind-safety-bug
Open

unwind safety fixes#140
Mirko-A wants to merge 3 commits intoRust-for-Linux:mainfrom
Mirko-A:mirko/fix-unwind-safety-bug

Conversation

@Mirko-A
Copy link
Copy Markdown

@Mirko-A Mirko-A commented Apr 27, 2026

Adds unwind-safety fixes to in-place initialization helpers so partially initialized values are dropped on both errors and panics. Changes:

  • Add __internal::ArrayInit guard type that safely initializes an array by running an initializer on each element, keeping track of the number of initialized elements. When dropped, if the initialization was partially successful, the guard drops the initialized portion of the array. Use the guard in [pin_]init_array_from_fn.
  • Use __internal::DropGuard in [pin_]chain so a panic/error in the chained closure drops the value initialized in the first stage; mem::forget the guard on success.
  • Add regression tests.

Closes: #136


The approach taken here was suggested in the issue itself, thus I added a Suggested-by tag. I hope that is okay.

Edits:
v1 -> v2:

  • rename ArrayInitGuard to ArrayInit
  • implement [Pin]Init for ArrayInit instead of wrapping it in [pin_]init_from_closure

v2 -> v3:

  • do not mem::forget the ArrayInit guard on success; state that the guard will only drop initialized elements on partial success

@Mirko-A Mirko-A force-pushed the mirko/fix-unwind-safety-bug branch from 235fcfb to 45e29c0 Compare April 27, 2026 19:38
@Mirko-A Mirko-A marked this pull request as ready for review April 27, 2026 20:00
@nbdd0121 nbdd0121 added the soundness Related to soundness issues label Apr 28, 2026
@Mirko-A Mirko-A force-pushed the mirko/fix-unwind-safety-bug branch 2 times, most recently from 7ad7985 to 225f13b Compare May 1, 2026 23:11
Comment thread src/__internal.rs Outdated
@Mirko-A Mirko-A force-pushed the mirko/fix-unwind-safety-bug branch from e45f41e to e3b2cb6 Compare May 3, 2026 12:45
Comment thread src/__internal.rs Outdated
Comment thread src/__internal.rs Outdated
///
/// Drops the already initialized elements of the array if an error or panic occurs
/// partway through the initialization process.
pub struct ArrayInit<T, F> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
pub struct ArrayInit<T, F> {
pub(crate) struct ArrayInit<T, F> {

Comment thread src/__internal.rs Outdated
unsafe { init.__init(ptr) }?;
self.num_init += 1;
}
core::mem::forget(self);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This incorrectly forgets F. You should just set self.ptr to null.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ouch. This is fixed, thank you for pointing it out.

Comment thread src/__internal.rs Outdated
Comment on lines +370 to +372
// SAFETY: Safety contract of `ArrayInit` guarantees that elements
// `self.ptr[0..self.num_init]` are initialized and contain valid `T`
// values, so dropping them is safe.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be represented as invariants of the type.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I've added an Invariants section to ArrayInit docs, I hope it is acceptable.

Comment thread src/lib.rs
let val = unsafe { Pin::new_unchecked(val) };
// SAFETY: `slot` was initialized above.
(self.1)(val).inspect_err(|_| unsafe { core::ptr::drop_in_place(slot) })
(self.1)(val)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The whole implementation here can probably be simplified to something like

(self.1)(unsafe { Slot::new(slot) }.init(self.0).let_binding()) or something similar if #143 lands.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I could rebase this branch to dev/accessor-rework if you'd like? Otherwise I don't mind waiting for #143 to land.

Comment thread src/__internal.rs Outdated
}

impl<T, F> ArrayInit<T, F> {
/// # Safety
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use whatever you actually require for safety requirement. This function can probably not be unsafe at all.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed unsafe from ArrayInit::new.

@Mirko-A Mirko-A force-pushed the mirko/fix-unwind-safety-bug branch from e3b2cb6 to e5f75a9 Compare May 5, 2026 13:35
@nbdd0121
Copy link
Copy Markdown
Member

nbdd0121 commented May 5, 2026

I cannot review diff of diffs. Please always squash your changes to individually meaningful commits.

@Mirko-A Mirko-A force-pushed the mirko/fix-unwind-safety-bug branch from e5f75a9 to 5c83836 Compare May 5, 2026 13:49
@Mirko-A
Copy link
Copy Markdown
Author

Mirko-A commented May 5, 2026

I see, everything has been squashed to the original three commits.

When reviewing, I prefer having diffs that do exactly what each of the comments suggests and squashing after the review. I will squash future fixups immediately.

@nbdd0121
Copy link
Copy Markdown
Member

nbdd0121 commented May 5, 2026

For GitHub-centric workflow that's indeed convenient, but for this project as the commits are synced to the kernel I need to review each commit individually so it's up to kernel's quality requirement.

Honestly the actual issue is that GitHub does not provide a good way to visualize the diff, despite that it has all the needed information to render as such. The "Compare" button sometimes does the job, but it breaks when you rebase and fixup commits in a single push. Longer term, either we can wait until GitHub implements it (which is quite unlikely given the slop), or we can adopt what the Rust Project's triagebot does by automatically generates a range diff like this.

@Mirko-A
Copy link
Copy Markdown
Author

Mirko-A commented May 5, 2026

I understand and fully agree. Unfortunately, looking at recent trends at GitHub, I don't expect much in terms of improvements to the platform.

Thank you for taking the time to explain your reasoning. :)

Mirko-A added 3 commits May 5, 2026 16:09
Adds a guard type that safely initializes an array by running an initializer
on each element, keeping track of the number of initialized elements. In the
case of a panic or error in the per-element initializer, the guard drops the
already-initialized portion of the array; nothing is dropped on success.

The previous code only ran cleanup on the explicit error path. If the per-
element initializer panicked partway through, the elements already written
into the array would be leaked: their `Drop` impls would never run.

Link: Rust-for-Linux#136
Reported-by: Gary Guo <gary@garyguo.net>
Suggested-by: Gary Guo <gary@garyguo.net>
Signed-off-by: Mirko Adzic <adzicmirko97@gmail.com>
Adds a drop guard before the call to the chained closure so that the
value initialized by the first stage is dropped if the closure errors
or panics; `mem::forget` the guard on success.

The previous code only ran cleanup on the explicit error path, leaking
the first-stage value if the chained closure panicked.

Link: Rust-for-Linux#136
Reported-by: Gary Guo <gary@garyguo.net>
Suggested-by: Gary Guo <gary@garyguo.net>
Signed-off-by: Mirko Adzic <adzicmirko97@gmail.com>
Cover both fixes added in the series:

- `[pin_]init_array_from_fn`: a panic or error from element `i`'s
  initializer drops the previously initialized elements `0..i`.
- `[pin_]chain`: a panic or error from the chained closure drops
  the value initialized by the first stage.

Also assert no double-drop on the success paths.

Signed-off-by: Mirko Adzic <adzicmirko97@gmail.com>
@Mirko-A Mirko-A force-pushed the mirko/fix-unwind-safety-bug branch from 5c83836 to eebe948 Compare May 5, 2026 14:11
Comment thread src/__internal.rs
Comment on lines +288 to +292
/// - `ptr` is null:
/// - before `__init` or `__pinned_init` is called
/// - after the array is successfully initialized
/// - `ptr[0..num_init]` contains initialized elements of type `T`
/// - `ptr[num_init..N]` (where N is the size of the array) contains uninitialized memory
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
/// - `ptr` is null:
/// - before `__init` or `__pinned_init` is called
/// - after the array is successfully initialized
/// - `ptr[0..num_init]` contains initialized elements of type `T`
/// - `ptr[num_init..N]` (where N is the size of the array) contains uninitialized memory
/// If `ptr` is not null:
/// - `ptr[0..num_init]` contains initialized elements of type `T`
/// - `ptr[num_init..N]` (where N is the size of the array) contains uninitialized memory

Comment thread src/__internal.rs
}

impl<T, F> ArrayInit<T, F> {
pub(crate) fn new(make_init: F) -> Self {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Needs ``#[inline]`

Comment thread src/__internal.rs
Comment on lines +325 to +326
// SAFETY: Since `0 <= i < N`, `self.ptr.add(i)` is in bounds and
// valid for writes by the safety contract of `__init`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not calling __init yet. So just mention about inbounds.

Comment thread src/__internal.rs

impl<T, F> ArrayInit<T, F> {
pub(crate) fn new(make_init: F) -> Self {
Self {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Need // INVARIANT

Comment thread src/__internal.rs
// SAFETY: The pointer is derived from `slot` and thus satisfies the
// `__init` requirements.
unsafe { init.__init(ptr) }?;
self.num_init += 1;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd actually either put self.num_init = i; at the beginning of the loop, or explicitly specify self.num_init = 0 at the beginning of the function.

Comment thread src/__internal.rs
// valid for writes by the safety contract of `__init`.
let ptr = unsafe { self.ptr.add(i) };
// SAFETY: The pointer is derived from `slot` and thus satisfies the
// `__init` requirements.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This needs to be more explicit and refers to the safety requirements of Init::__init.

Comment thread src/__internal.rs
F: FnMut(usize) -> I,
I: PinInit<T, E>,
{
unsafe fn __pinned_init(mut self, slot: *mut [T; N]) -> Result<(), E> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ditto for this function

Comment thread src/__internal.rs
Comment on lines +373 to +375
// SAFETY: Invariants of `ArrayInit` guarantee that elements
// `self.ptr[0..self.num_init]` are initialized and contain valid `T`
// values, so dropping them is safe.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps also mention before the safety comment that "In this code path, the initialization has failed partway and we need to drop elements initialized thus far to uphold the pinning requirement."

Comment thread tests/unwind_safety.rs
static DROPS: AtomicUsize = AtomicUsize::new(0);
/// Serialize tests so the shared [DROPS] counter stays meaningful even
/// when cargo runs tests in parallel.
static LOCK: Mutex<()> = Mutex::new(());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you make Counted something like:

struct Counted<'a>(&'a AtomicUsize);

to avoid things like this?

Comment thread tests/unwind_safety.rs

impl Drop for Counted {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::SeqCst);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Relaxed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

soundness Related to soundness issues

Development

Successfully merging this pull request may close these issues.

pin_init_array_from_fn and pin_chain are not unwind-safe

2 participants