From mboxrd@z Thu Jan 1 00:00:00 1970 From: Claude Code Review Bot To: dri-devel-reviews@example.com Subject: Claude review: rust: sync: Introduce LazyInit Date: Wed, 27 May 2026 14:07:41 +1000 Message-ID: In-Reply-To: <20260526205419.1055109-3-lyude@redhat.com> References: <20260526205419.1055109-1-lyude@redhat.com> <20260526205419.1055109-3-lyude@redhat.com> X-Mailer: Claude Code Patch Reviewer Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Patch Review This is the main patch. A thorough review: **1. Potential soundness issue in `reset()`** (lazy_init.rs:253-264): ```rust pub fn reset(self: Pin<&mut Self>) -> bool { let inner =3D self.project().inner.get_mut_pinned(); let was_init =3D inner.is_init; // SAFETY: // - We drop in place, and therefore do not move `inner` // - The `PinnedDrop` implementation of `Inner` leaves it in a well-def= ined // state, so we do not need to worry about UB from further usage. unsafe { ptr::drop_in_place(inner.get_unchecked_mut()) }; was_init } ``` After `drop_in_place`, the `Inner` is left in a dropped state. The `PinnedD= rop` impl does set `is_init =3D false`, which means subsequent `init()` cal= ls on the re-used `LazyInit` will try to re-initialize. However, the `Mutex= ` *itself* is **not** dropped or re-initialized here =E2=80=94 only its con= tents (`Inner`) are dropped in place. The question is whether the `Mutex`'s= internal C state remains valid after its protected data has been dropped-i= n-place and a new value written into it via a subsequent `init()` call. Since the `Mutex` wraps `Inner` and the lock state is separate from the dat= a, this should be fine for the mutex itself. But there's a subtler concern:= `Inner` contains `MaybeUninit` which after `PinnedDrop` runs `assume_in= it_drop()` will have dropped `T` but the `MaybeUninit` bytes are still ther= e (just logically uninitialized). The `is_init` flag being set to `false` i= n `PinnedDrop` ensures the invariant is maintained. Then when `init()` is c= alled again, `__pinned_init` writes to `data.as_mut_ptr()` which writes ove= r the `MaybeUninit` =E2=80=94 this is fine because `MaybeUninit` allows wri= ting at any time. The real concern: after `drop_in_place`, the `Inner` struct's own drop flag= (if any) or internal state may be in an undefined state for further use. I= n Rust, using a value after `drop_in_place` is valid only if you ensure no = further drop will occur or you re-initialize it properly. Here, `LazyInit` = itself will still be dropped eventually, which drops the `Mutex`, which dro= ps its `Inner`. But `Inner` was already dropped in place. If `PinnedDrop` i= s called again on the already-dropped `Inner`, this is double-drop UB. Actually, looking more carefully: `reset()` drops `Inner` in place via `ptr= ::drop_in_place`. Then when the `LazyInit` goes out of scope, the `Mutex` i= s dropped, which will try to drop its `Inner` contents again. If `is_init` = is `false` (set by `PinnedDrop`), the second drop will skip the `assume_ini= t_drop` path. But the `PinnedDrop` trait itself will still be invoked a sec= ond time =E2=80=94 is that safe? The `PinnedDrop` impl only conditionally d= rops based on `is_init`, so the second call will be a no-op on the data. Th= e `is_init =3D false` assignment is idempotent. So this should be safe **pr= ovided** that calling `PinnedDrop` on an already-dropped `Inner` does nothi= ng harmful beyond the `is_init` check. However, this pattern of `drop_in_place` followed by re-use is fragile. Con= sider documenting this more explicitly, or using a more conventional approa= ch like `mem::replace` or swapping in an uninitialized `Inner`. **2. Lifetime extension in `data()` uses `mem::transmute`** (lazy_init.rs:1= 72-179): ```rust unsafe fn data<'a>(&'a self, inner: &MutexGuard<'_, Inner>) -> &'a T { unsafe { mem::transmute::<&_, &'a _>(inner.data.assume_init_ref()) } } ``` This transmute extends the lifetime of the reference from the `MutexGuard`'= s lifetime to `'a` (the `LazyInit`'s lifetime). The safety argument is that= once initialized, the data is never moved or dropped until `LazyInit` itse= lf is dropped, and `T: Send + Sync` makes concurrent access safe. The concern is that the `MutexGuard` is dropped (unlocking the mutex) while= the extended-lifetime reference still exists. Then another thread could ca= ll `init()`, acquire the lock, and =E2=80=94 well, `init()` checks `is_init= ` and short-circuits if already initialized, so the data won't be overwritt= en. The invariant holds. But if `reset()` were ever callable from `&self` r= ather than `Pin<&mut Self>`, this would be unsound. The `&mut` requirement = on `reset()` is load-bearing for this safety argument =E2=80=94 worth a com= ment on `data()` noting this dependency. **3. Documentation typos/nits** (lazy_init.rs:88-89): ``` /// multiple callers attempting to initialize at the same time will block u= ntil the conetents of the ``` "conetents" =E2=86=92 "contents" (lazy_init.rs:97): ``` /// by the lock, and thus `T` must provide it's own synchronization ``` "it's" =E2=86=92 "its" (possessive, not contraction) **4. Stale reference to `LazyInit::populate()` in `PinnedDrop`** (lazy_init= .rs:45-46): ```rust // INVARIANT: This is the only place other then `LazyInit::populate()` wher= e we write to // `data`, and this function requires &mut self - ensuring it cannot race w= ith // `LazyInit::populate()`. ``` There is no `LazyInit::populate()` method. This should reference `LazyInit:= :init()`. **5. The `LazyInitError` doc says "during `LazyInit::new`"** (lazy_init.rs:= 56): ```rust /// Errors that can occur during [`LazyInit::new`]. ``` This should be "during [`LazyInit::init`]" =E2=80=94 `new()` returns an ini= tializer and doesn't produce `LazyInitError`. **6. Glob re-export** (sync.rs): ```rust pub use lazy_init::*; ``` This is a glob re-export from a non-`pub` module. While it works, the kerne= l Rust style generally prefers explicit re-exports (as seen with all the ot= her items in the same block: `Arc`, `CondVar`, `Mutex`, etc.). Consider `pu= b use lazy_init::{LazyInit, LazyInitError, new_lazy_init};` for consistency. **7. Unused import in tests** (lazy_init.rs:269): ```rust use core::ops::Deref; ``` `Deref` does not appear to be explicitly used in the test module. The `(*on= ce).as_ref()` calls use the deref operator `*`, but that doesn't require an= explicit `use core::ops::Deref` import. **8. Match pattern style** (lazy_init.rs:309, 336): ```rust r @ _ =3D> panic!("..."), ``` `r @ _` is equivalent to just `r`. The `@ _` binding is unnecessary. **9. The `init()` return type semantics are slightly unusual**: Returning `= Err(AlreadyInit(&T))` when the value was already initialized makes the "alr= eady init" path an error, but the caller often doesn't care and just wants = the reference. Users will frequently need `.unwrap_or_else(|e| match e { Al= readyInit(v) =3D> v, DuringInit(e) =3D> return Err(e) })`. Consider whether= a method like `get_or_init()` that returns `Result<&T, E>` (mapping Alread= yInit to Ok) would be a more ergonomic primary API, with the current `init(= )` as an `init_unique()` or similar for callers that need to distinguish. **Verdict**: Mostly sound with good safety documentation. The `populate()` = reference is clearly a leftover from development, and the `LazyInit::new` d= oc on `LazyInitError` should be fixed. The `reset()` + double-drop pattern = deserves closer examination but appears safe given the `PinnedDrop` impleme= ntation's idempotent behavior. The lifetime extension via transmute is the = trickiest part of this patch but is correctly guarded by the `&mut Self` re= quirement on `reset()`. --- Generated by Claude Code Patch Reviewer