Skip to content

Commit

Permalink
Split large unsafe function slide (google#2406)
Browse files Browse the repository at this point in the history
The old slice was doing several things at the same time: demonstrating
both external functions as well as unsafe Rust functions.

We now treat those two topics separately. In addition, the “Calling
Unsafe Functions” heading has become its own slide with a non-crashing
example that shows what can go wrong if an argument is misunderstood
in a call to an unsafe function. The old example didn’t actually
illustrate the danger clearly: it would produce mangled UTF-8 output,
which the Playground server refuses to print.

Part of google#2445.

---------

Co-authored-by: Dustin J. Mitchell <[email protected]>
Co-authored-by: Andrew Walbran <[email protected]>
  • Loading branch information
3 people authored and michael-kerscher committed Feb 7, 2025
1 parent e3fc55e commit 33221d1
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 87 deletions.
3 changes: 3 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@
- [Mutable Static Variables](unsafe-rust/mutable-static.md)
- [Unions](unsafe-rust/unions.md)
- [Unsafe Functions](unsafe-rust/unsafe-functions.md)
- [Unsafe Rust Functions](unsafe-rust/unsafe-functions/rust.md)
- [Unsafe External Functions](unsafe-rust/unsafe-functions/extern-c.md)
- [Calling Unsafe Functions](unsafe-rust/unsafe-functions/calling.md)
- [Unsafe Traits](unsafe-rust/unsafe-traits.md)
- [Exercise: FFI Wrapper](unsafe-rust/exercise.md)
- [Solution](unsafe-rust/solution.md)
Expand Down
93 changes: 6 additions & 87 deletions src/unsafe-rust/unsafe-functions.md
Original file line number Diff line number Diff line change
@@ -1,100 +1,19 @@
---
minutes: 5
minutes: 15
---

# Unsafe Functions

## Calling Unsafe Functions

A function or method can be marked `unsafe` if it has extra preconditions you
must uphold to avoid undefined behaviour:

```rust,editable
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
let emojis = "🗻∈🌏";
// SAFETY: The indices are in the correct order, within the bounds of the
// string slice, and lie on UTF-8 sequence boundaries.
unsafe {
println!("emoji: {}", emojis.get_unchecked(0..4));
println!("emoji: {}", emojis.get_unchecked(4..7));
println!("emoji: {}", emojis.get_unchecked(7..11));
}
println!("char count: {}", count_chars(unsafe { emojis.get_unchecked(0..7) }));
// SAFETY: `abs` doesn't deal with pointers and doesn't have any safety
// requirements.
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
// Not upholding the UTF-8 encoding requirement breaks memory safety!
// println!("emoji: {}", unsafe { emojis.get_unchecked(0..3) });
// println!("char count: {}", count_chars(unsafe {
// emojis.get_unchecked(0..3) }));
}
fn count_chars(s: &str) -> usize {
s.chars().count()
}
```

## Writing Unsafe Functions
must uphold to avoid undefined behaviour.

You can mark your own functions as `unsafe` if they require particular
conditions to avoid undefined behaviour.
There are two main categories:

```rust,editable
/// Swaps the values pointed to by the given pointers.
///
/// # Safety
///
/// The pointers must be valid and properly aligned.
unsafe fn swap(a: *mut u8, b: *mut u8) {
let temp = *a;
*a = *b;
*b = temp;
}
fn main() {
let mut a = 42;
let mut b = 66;
// SAFETY: ...
unsafe {
swap(&mut a, &mut b);
}
println!("a = {}, b = {}", a, b);
}
```
- Rust functions declared unsafe with `unsafe fn`.
- Foreign functions in `extern "C"` blocks.

<details>

## Calling Unsafe Functions

`get_unchecked`, like most `_unchecked` functions, is unsafe, because it can
create UB if the range is incorrect. `abs` is unsafe for a different reason: it
is an external function (FFI). Calling external functions is usually only a
problem when those functions do things with pointers which might violate Rust's
memory model, but in general any C function might have undefined behaviour under
any arbitrary circumstances.

The `"C"` in this example is the ABI;
[other ABIs are available too](https://doc.rust-lang.org/reference/items/external-blocks.html).

## Writing Unsafe Functions

We wouldn't actually use pointers for a `swap` function - it can be done safely
with references.

Note that unsafe code is allowed within an unsafe function without an `unsafe`
block. We can prohibit this with `#[deny(unsafe_op_in_unsafe_fn)]`. Try adding
it and see what happens. This will likely change in a future Rust edition.
We will look at the two kinds of unsafe functions next.

</details>
48 changes: 48 additions & 0 deletions src/unsafe-rust/unsafe-functions/calling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Calling Unsafe Functions

Failing to uphold the safety requirements breaks memory safety!

```rust,editable
#[derive(Debug)]
#[repr(C)]
struct KeyPair {
pk: [u16; 4], // 8 bytes
sk: [u16; 4], // 8 bytes
}
const PK_BYTE_LEN: usize = 8;
fn log_public_key(pk_ptr: *const u16) {
let pk: &[u16] = unsafe { std::slice::from_raw_parts(pk_ptr, PK_BYTE_LEN) };
println!("{pk:?}");
}
fn main() {
let key_pair = KeyPair { pk: [1, 2, 3, 4], sk: [0, 0, 42, 0] };
log_public_key(key_pair.pk.as_ptr());
}
```

Always include a safety comment for each `unsafe` block. It must explain why the
code is actually safe. This example is missing a safety comment and is unsound.

<details>

Key points:

- The second argument to `slice::from_raw_parts` is the number of _elements_,
not bytes! This example demonstrates unexpected behavior by reading past the
end of one array and into another.
- This is not actually undefined behaviour, as `KeyPair` has a defined
representation (due to `repr(C)`) and no padding, so the contents of the
second array is also valid to read through the same pointer.
- `log_public_key` should be unsafe, because `pk_ptr` must meet certain
prerequisites to avoid undefined behaviour. A safe function which can cause
undefined behaviour is said to be `unsound`. What should its safety
documentation say?
- The standard library contains many low-level unsafe functions. Prefer the safe
alternatives when possible!
- If you use an unsafe function as an optimization, make sure to add a benchmark
to demonstrate the gain.

</details>
44 changes: 44 additions & 0 deletions src/unsafe-rust/unsafe-functions/extern-c.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Unsafe External Functions

Functions in a foreign language may also be unsafe:

```rust,editable
use std::ffi::c_char;
unsafe extern "C" {
// `abs` doesn't deal with pointers and doesn't have any safety requirements.
safe fn abs(input: i32) -> i32;
/// # Safety
///
/// `s` must be a pointer to a NUL-terminated C string which is valid and
/// not modified for the duration of this function call.
unsafe fn strlen(s: *const c_char) -> usize;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
unsafe {
// SAFETY: We pass a pointer to a C string literal which is valid for
// the duration of the program.
println!("String length: {}", strlen(c"String".as_ptr()));
}
}
```

<details>

- Rust used to consider all extern functions unsafe, but this changed in Rust
1.82 with `unsafe extern` blocks.
- `abs` must be explicitly marked as `safe` because it is an external function
(FFI). Calling external functions is usually only a problem when those
functions do things with pointers which which might violate Rust's memory
model, but in general any C function might have undefined behaviour under any
arbitrary circumstances.
- The `"C"` in this example is the ABI;
[other ABIs are available too](https://doc.rust-lang.org/reference/items/external-blocks.html).
- Note that there is no verification that the Rust function signature matches
that of the function definition -- that's up to you!

</details>
43 changes: 43 additions & 0 deletions src/unsafe-rust/unsafe-functions/rust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Unsafe Rust Functions

You can mark your own functions as `unsafe` if they require particular
preconditions to avoid undefined behaviour.

```rust,editable
/// Swaps the values pointed to by the given pointers.
///
/// # Safety
///
/// The pointers must be valid, properly aligned, and not otherwise accessed for
/// the duration of the function call.
unsafe fn swap(a: *mut u8, b: *mut u8) {
let temp = *a;
*a = *b;
*b = temp;
}
fn main() {
let mut a = 42;
let mut b = 66;
// SAFETY: The pointers must be valid, aligned and unique because they came
// from references.
unsafe {
swap(&mut a, &mut b);
}
println!("a = {}, b = {}", a, b);
}
```

<details>

We wouldn't actually use pointers for a `swap` function --- it can be done
safely with references.

Note that unsafe code is allowed within an unsafe function without an `unsafe`
block. We can prohibit this with `#[deny(unsafe_op_in_unsafe_fn)]`. Try adding
it and see what happens. This will
[change in the 2024 Rust edition](https://github.com/rust-lang/rust/issues/120535).

</details>

0 comments on commit 33221d1

Please sign in to comment.