Repository: rust-lang/rust
Pull request #139145: slice: Remove some uses of unsafe in first/last chunk methods
Author: login: okaneco
Labels: name: T-compiler
description: Relevant to the compiler team, which will review and decide on the PR/issue.
name: S-waiting-on-bors
description: Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
name: T-libs
description: Relevant to the library team, which will review and decide on the PR/issue.
Created: 2025-03-30T17:01:15Z | Merged: 2025-04-03T19:16:29Z

Remove unsafe `split_at_unchecked` and `split_at_mut_unchecked` in some slice `split_first_chunk`/`split_last_chunk` methods.
Replace those calls with the safe `split_at` and `split_at_checked` where applicable.

Add codegen tests to check for no panics when calculating the last chunk index using `checked_sub` and `split_at`.

Better viewed with whitespace disabled in diff view 

--- 

The unchecked calls are mostly manual implementations of the safe methods, but with the safety condition negated from `mid <= len` to `len < mid`.
```rust
if self.len() < N {
    None
} else {
    // SAFETY: We manually verified the bounds of the split.
    let (first, tail) = unsafe { self.split_at_unchecked(N) };
    // Or for the last_chunk methods
    let (init, last) = unsafe { self.split_at_unchecked(self.len() - N) };
```

Unsafe is still needed for the pointer array casts. Their safety comments are unmodified.


Diff:
diff --git a/library/core/src/slice/mod.rs b/library/core/src/slice/mod.rs
index 5bb7243c4491b..4e8be20643179 100644
--- a/library/core/src/slice/mod.rs
+++ b/library/core/src/slice/mod.rs
@@ -382,16 +382,11 @@ impl<T> [T] {
     #[stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     #[rustc_const_stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     pub const fn split_first_chunk<const N: usize>(&self) -> Option<(&[T; N], &[T])> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the split.
-            let (first, tail) = unsafe { self.split_at_unchecked(N) };
+        let Some((first, tail)) = self.split_at_checked(N) else { return None };
 
-            // SAFETY: We explicitly check for the correct number of elements,
-            //   and do not let the references outlive the slice.
-            Some((unsafe { &*(first.as_ptr().cast::<[T; N]>()) }, tail))
-        }
+        // SAFETY: We explicitly check for the correct number of elements,
+        //   and do not let the references outlive the slice.
+        Some((unsafe { &*(first.as_ptr().cast::<[T; N]>()) }, tail))
     }
 
     /// Returns a mutable array reference to the first `N` items in the slice and the remaining
@@ -419,17 +414,12 @@ impl<T> [T] {
     pub const fn split_first_chunk_mut<const N: usize>(
         &mut self,
     ) -> Option<(&mut [T; N], &mut [T])> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the split.
-            let (first, tail) = unsafe { self.split_at_mut_unchecked(N) };
+        let Some((first, tail)) = self.split_at_mut_checked(N) else { return None };
 
-            // SAFETY: We explicitly check for the correct number of elements,
-            //   do not let the reference outlive the slice,
-            //   and enforce exclusive mutability of the chunk by the split.
-            Some((unsafe { &mut *(first.as_mut_ptr().cast::<[T; N]>()) }, tail))
-        }
+        // SAFETY: We explicitly check for the correct number of elements,
+        //   do not let the reference outlive the slice,
+        //   and enforce exclusive mutability of the chunk by the split.
+        Some((unsafe { &mut *(first.as_mut_ptr().cast::<[T; N]>()) }, tail))
     }
 
     /// Returns an array reference to the last `N` items in the slice and the remaining slice.
@@ -452,16 +442,12 @@ impl<T> [T] {
     #[stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     #[rustc_const_stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     pub const fn split_last_chunk<const N: usize>(&self) -> Option<(&[T], &[T; N])> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the split.
-            let (init, last) = unsafe { self.split_at_unchecked(self.len() - N) };
+        let Some(index) = self.len().checked_sub(N) else { return None };
+        let (init, last) = self.split_at(index);
 
-            // SAFETY: We explicitly check for the correct number of elements,
-            //   and do not let the references outlive the slice.
-            Some((init, unsafe { &*(last.as_ptr().cast::<[T; N]>()) }))
-        }
+        // SAFETY: We explicitly check for the correct number of elements,
+        //   and do not let the references outlive the slice.
+        Some((init, unsafe { &*(last.as_ptr().cast::<[T; N]>()) }))
     }
 
     /// Returns a mutable array reference to the last `N` items in the slice and the remaining
@@ -489,17 +475,13 @@ impl<T> [T] {
     pub const fn split_last_chunk_mut<const N: usize>(
         &mut self,
     ) -> Option<(&mut [T], &mut [T; N])> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the split.
-            let (init, last) = unsafe { self.split_at_mut_unchecked(self.len() - N) };
+        let Some(index) = self.len().checked_sub(N) else { return None };
+        let (init, last) = self.split_at_mut(index);
 
-            // SAFETY: We explicitly check for the correct number of elements,
-            //   do not let the reference outlive the slice,
-            //   and enforce exclusive mutability of the chunk by the split.
-            Some((init, unsafe { &mut *(last.as_mut_ptr().cast::<[T; N]>()) }))
-        }
+        // SAFETY: We explicitly check for the correct number of elements,
+        //   do not let the reference outlive the slice,
+        //   and enforce exclusive mutability of the chunk by the split.
+        Some((init, unsafe { &mut *(last.as_mut_ptr().cast::<[T; N]>()) }))
     }
 
     /// Returns an array reference to the last `N` items in the slice.
@@ -522,17 +504,13 @@ impl<T> [T] {
     #[stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     #[rustc_const_stable(feature = "const_slice_last_chunk", since = "1.80.0")]
     pub const fn last_chunk<const N: usize>(&self) -> Option<&[T; N]> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the slice.
-            // FIXME(const-hack): Without const traits, we need this instead of `get_unchecked`.
-            let last = unsafe { self.split_at_unchecked(self.len() - N).1 };
+        // FIXME(const-hack): Without const traits, we need this instead of `get`.
+        let Some(index) = self.len().checked_sub(N) else { return None };
+        let (_, last) = self.split_at(index);
 
-            // SAFETY: We explicitly check for the correct number of elements,
-            //   and do not let the references outlive the slice.
-            Some(unsafe { &*(last.as_ptr().cast::<[T; N]>()) })
-        }
+        // SAFETY: We explicitly check for the correct number of elements,
+        //   and do not let the references outlive the slice.
+        Some(unsafe { &*(last.as_ptr().cast::<[T; N]>()) })
     }
 
     /// Returns a mutable array reference to the last `N` items in the slice.
@@ -556,18 +534,14 @@ impl<T> [T] {
     #[stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     #[rustc_const_stable(feature = "const_slice_first_last_chunk", since = "1.83.0")]
     pub const fn last_chunk_mut<const N: usize>(&mut self) -> Option<&mut [T; N]> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the slice.
-            // FIXME(const-hack): Without const traits, we need this instead of `get_unchecked`.
-            let last = unsafe { self.split_at_mut_unchecked(self.len() - N).1 };
-
-            // SAFETY: We explicitly check for the correct number of elements,
-            //   do not let the reference outlive the slice,
-            //   and require exclusive access to the entire slice to mutate the chunk.
-            Some(unsafe { &mut *(last.as_mut_ptr().cast::<[T; N]>()) })
-        }
+        // FIXME(const-hack): Without const traits, we need this instead of `get`.
+        let Some(index) = self.len().checked_sub(N) else { return None };
+        let (_, last) = self.split_at_mut(index);
+
+        // SAFETY: We explicitly check for the correct number of elements,
+        //   do not let the reference outlive the slice,
+        //   and require exclusive access to the entire slice to mutate the chunk.
+        Some(unsafe { &mut *(last.as_mut_ptr().cast::<[T; N]>()) })
     }
 
     /// Returns a reference to an element or subslice depending on the type of
diff --git a/tests/codegen/slice-split-at.rs b/tests/codegen/slice-split-at.rs
new file mode 100644
index 0000000000000..07018cf9c9171
--- /dev/null
+++ b/tests/codegen/slice-split-at.rs
@@ -0,0 +1,24 @@
+//@ compile-flags: -Copt-level=3
+#![crate_type = "lib"]
+
+// Check that no panic is generated in `split_at` when calculating the index for
+// the tail chunk using `checked_sub`.
+//
+// Tests written for refactored implementations of:
+// `<[T]>::{split_last_chunk, split_last_chunk_mut, last_chunk, last_chunk_mut}`
+
+// CHECK-LABEL: @split_at_last_chunk
+#[no_mangle]
+pub fn split_at_last_chunk(s: &[u8], chunk_size: usize) -> Option<(&[u8], &[u8])> {
+    // CHECK-NOT: panic
+    let Some(index) = s.len().checked_sub(chunk_size) else { return None };
+    Some(s.split_at(index))
+}
+
+// CHECK-LABEL: @split_at_mut_last_chunk
+#[no_mangle]
+pub fn split_at_mut_last_chunk(s: &mut [u8], chunk_size: usize) -> Option<(&mut [u8], &mut [u8])> {
+    // CHECK-NOT: panic
+    let Some(index) = s.len().checked_sub(chunk_size) else { return None };
+    Some(s.split_at_mut(index))
+}


Reviews:
user: login: Amanieu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T10:52:54Z
user: login: okaneco
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:35:47Z
user: login: Amanieu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T19:17:02Z

Review comments:
diff_hunk: @@ -382,16 +382,11 @@ impl<T> [T] {
     #[stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     #[rustc_const_stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     pub const fn split_first_chunk<const N: usize>(&self) -> Option<(&[T; N], &[T])> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the split.
-            let (first, tail) = unsafe { self.split_at_unchecked(N) };
+        let Some((first, tail)) = self.split_at_checked(N) else { return None };
path: library/core/src/slice/mod.rs
user: login: Amanieu
body: The code for all of these can be made more concise by using `?`:
```suggestion
        let (first, tail) = self.split_at_checked(N)?;
```
created_at: 2025-04-02T10:51:55Z
author_association: MEMBER
diff_hunk: @@ -382,16 +382,11 @@ impl<T> [T] {
     #[stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     #[rustc_const_stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     pub const fn split_first_chunk<const N: usize>(&self) -> Option<(&[T; N], &[T])> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the split.
-            let (first, tail) = unsafe { self.split_at_unchecked(N) };
+        let Some((first, tail)) = self.split_at_checked(N) else { return None };
path: library/core/src/slice/mod.rs
user: login: okaneco
body: These functions are `const fn` so I don't believe `?` is available to use.

That results in the following error:
```rust
error[E0015]: `?` is not allowed on `Option<(&[T], &[T])>` in constant functions
   --> library\core\src\slice\mod.rs:385:29
    |
385 |         let (first, tail) = self.split_at_checked(N)?;
    |                             ^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants

```
created_at: 2025-04-02T15:35:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -382,16 +382,11 @@ impl<T> [T] {
     #[stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     #[rustc_const_stable(feature = "slice_first_last_chunk", since = "1.77.0")]
     pub const fn split_first_chunk<const N: usize>(&self) -> Option<(&[T; N], &[T])> {
-        if self.len() < N {
-            None
-        } else {
-            // SAFETY: We manually verified the bounds of the split.
-            let (first, tail) = unsafe { self.split_at_unchecked(N) };
+        let Some((first, tail)) = self.split_at_checked(N) else { return None };
path: library/core/src/slice/mod.rs
user: login: Amanieu
body: Good point! I'd forgotten about that.
created_at: 2025-04-02T19:17:02Z
author_association: MEMBER

PR comments:
user: login: rustbot
created_at: 2025-03-30T17:01:19Z
body: r? @Amanieu

rustbot has assigned @Amanieu.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use `r?` to explicitly pick a reviewer
author_association: COLLABORATOR
user: login: Amanieu
created_at: 2025-04-02T20:04:22Z
body: @bors r+
author_association: MEMBER
user: login: bors
created_at: 2025-04-02T20:04:24Z
body: :pushpin: Commit 59ca7679c7db634465b5f021060f143567824ac4 has been approved by `Amanieu`

It is now in the [queue](https://bors.rust-lang.org/homu/queue/rust) for this repository.

<!-- @bors r=Amanieu 59ca7679c7db634465b5f021060f143567824ac4 -->
<!-- homu: {"type":"Approved","sha":"59ca7679c7db634465b5f021060f143567824ac4","approver":"Amanieu","queue":"https://bors.rust-lang.org/homu/queue/rust"} -->
author_association: COLLABORATOR