Skip to content

fix(lance-core): avoid aliasing &mut Runtime in global_cpu_runtime#7682

Merged
westonpace merged 1 commit into
lance-format:mainfrom
LuciferYang:fix/lance-core-cpu-runtime-aliasing-mut
Jul 10, 2026
Merged

fix(lance-core): avoid aliasing &mut Runtime in global_cpu_runtime#7682
westonpace merged 1 commit into
lance-format:mainfrom
LuciferYang:fix/lance-core-cpu-runtime-aliasing-mut

Conversation

@LuciferYang

@LuciferYang LuciferYang commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What & why

lance-core keeps a process-global CPU thread-pool runtime behind CPU_RUNTIME: AtomicPtr<Runtime>. global_cpu_runtime() dereferenced that raw pointer into &'static mut Runtime. Its only caller, spawn_cpu, runs concurrently on many worker threads, so multiple &'static mut references to the same Runtime could be live at once — a violation of &mut uniqueness, which is undefined behavior on the Rust abstract machine even though every Runtime method actually used takes &self and never mutates. It is latent UB the compiler is permitted to miscompile under noalias assumptions (Miri flags it), not an observed crash today.

How & why it is correct

Return &'static Runtime and dereference with &*ptr instead. Three facts make this sound:

  • The only method called is Runtime::spawn_blocking, which takes &self — the &mut was never needed.
  • Runtime: Sync, so many threads may hold &'static Runtime concurrently without UB, which is exactly the aliasing the old code got wrong.
  • The runtime is created via Box::into_raw and never reclaimed, so the &'static lifetime is genuine (no use-after-free); atfork_tokio_child only nulls the pointer, it does not free.

Both unsafe derefs now carry // SAFETY: comments documenting these invariants.

Compatibility

Fully backward compatible, zero behavior change. global_cpu_runtime is private, so the return-type change is internal-only; spawn_cpu's public signature is unchanged. &mut Runtime already auto-reborrowed to &self for spawn_blocking, so narrowing to &Runtime is byte-for-byte identical at runtime — purely tightening a gratuitous, unsound &mut into a shared &.

Test plan

  • cargo test -p lance-core
  • cargo clippy -p lance-core --tests -- -D warnings
  • cargo fmt --all -- --check

@github-actions github-actions Bot added the bug Something isn't working label Jul 8, 2026
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@wjones127 wjones127 self-requested a review July 8, 2026 14:49

@westonpace westonpace left a comment

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.

Just some minor nits, and let's get rid of the test. The comment even explains that the old code would still have passed. We have basic happy path testing of spawn_cpu implicitly in plenty of other tests.

Comment thread rust/lance-core/src/utils/tokio.rs Outdated
Comment on lines +138 to +172

#[cfg(test)]
mod tests {
use super::*;

// Drives many `spawn_cpu` calls across real worker threads. Each task calls
// `global_cpu_runtime()` on its own thread, so before the fix (`&'static mut
// Runtime`) their transient `&mut` references to the one shared runtime could be
// live simultaneously — aliasing `&mut`, which violates `&mut` uniqueness. The
// fix returns a shared `&'static Runtime`, which many threads may hold at once.
//
// This is a functional/concurrency check, not a deterministic regression test:
// the aliasing was latent UB, so a plain `cargo test` passes on the old code too
// (the miscompilation it permits is not forced), and tokio's multi-thread runtime
// is not runnable under Miri. It guards against a future refactor breaking the
// concurrent path and documents the intended shared-reference contract.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn spawn_cpu_shared_runtime_is_concurrent_safe() {
let handles: Vec<_> = (0..64)
.map(|i| {
tokio::spawn(async move {
spawn_cpu(move || Ok::<_, std::convert::Infallible>(i * 2))
.await
.unwrap()
})
})
.collect();
let mut results = Vec::with_capacity(handles.len());
for handle in handles {
results.push(handle.await.unwrap());
}
results.sort_unstable();
assert_eq!(results, (0..64).map(|i| i * 2).collect::<Vec<_>>());
}
}

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 don't think we need this test (I don't think it is actually testing anything is it?)

Comment thread rust/lance-core/src/utils/tokio.rs Outdated
Comment on lines +76 to +79
// `Runtime` lives for the rest of the process — a `&'static` shared
// reference is valid. Multiple threads may hold this reference at once;
// that is sound because `Runtime` is `Sync` and every method used
// (`spawn_blocking`) takes `&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.

Suggested change
// `Runtime` lives for the rest of the process — a `&'static` shared
// reference is valid. Multiple threads may hold this reference at once;
// that is sound because `Runtime` is `Sync` and every method used
// (`spawn_blocking`) takes `&self`.
// `Runtime` lives for the rest of the process

The first part of this comment is interesting (may not always be obvious to readers that static state is leaked on fork) but the second part seems to just be redefining the concept of a shared reference.

Comment thread rust/lance-core/src/utils/tokio.rs Outdated
Comment on lines +92 to +94
// SAFETY: `new_ptr` was just obtained from `Box::into_raw`, so it is non-null,
// aligned, and points to a live `Runtime` that is never reclaimed. See the
// load branch above for why a shared `&'static` reference is the correct form.

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
// SAFETY: `new_ptr` was just obtained from `Box::into_raw`, so it is non-null,
// aligned, and points to a live `Runtime` that is never reclaimed. See the
// load branch above for why a shared `&'static` reference is the correct form.
// SAFETY: `new_ptr` was just obtained from `Box::into_raw`, so it is non-null,
// aligned, and points to a live `Runtime` that is never reclaimed.

@wjones127 wjones127 removed their request for review July 8, 2026 16:02
global_cpu_runtime() returned &'static mut Runtime by dereferencing a
process-global AtomicPtr<Runtime>. Its only caller, spawn_cpu, runs on
many worker threads concurrently, so multiple &'static mut references to
the same Runtime could be live at once — a violation of &mut uniqueness,
which is undefined behavior on the abstract machine even though every
Runtime method used takes &self and does not mutate.

Return &'static Runtime and dereference with &*ptr instead. spawn_blocking
takes &self, and Runtime is Sync, so many threads may share the reference
soundly. The runtime is created via Box::into_raw and never reclaimed, so
the &'static lifetime is genuine. Both unsafe derefs now carry SAFETY
comments documenting these invariants.
@LuciferYang LuciferYang force-pushed the fix/lance-core-cpu-runtime-aliasing-mut branch from b4179b3 to 4d2eef2 Compare July 9, 2026 03:01
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The function global_cpu_runtime() in rust/lance-core/src/utils/tokio.rs is changed to return a shared &'static Runtime instead of &'static mut Runtime. Both unsafe pointer dereferences within the function are updated accordingly, with added safety comments explaining pointer provenance and lifetime guarantees.

Changes

Runtime accessor mutability change

Layer / File(s) Summary
Shared reference conversion in global_cpu_runtime
rust/lance-core/src/utils/tokio.rs
Return type changes from &'static mut Runtime to &'static Runtime; both unsafe dereferences change from &mut *ptr/&mut *new_ptr to &*ptr/&*new_ptr, with added SAFETY comments on pointer provenance and lifetime.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the core fix in global_cpu_runtime by removing the aliased mutable Runtime reference.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@LuciferYang

Copy link
Copy Markdown
Contributor Author

Thanks for the review! @westonpace Addressed all three: dropped the test, and trimmed both SAFETY comments as suggested. Also rebased onto latest main.

@LuciferYang LuciferYang requested a review from westonpace July 10, 2026 09:18

@westonpace westonpace left a comment

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.

Thanks!

@westonpace westonpace merged commit 38f7bea into lance-format:main Jul 10, 2026
38 checks passed
@LuciferYang

Copy link
Copy Markdown
Contributor Author

Thank you @westonpace

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants