Description
See the code example:
#[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());
}
The sample shows a call to std::slice::from_raw_parts
that does not fulfill that function's precondition; namely:
The
len
argument is the number of elements, not the number of bytes.
In this case, the size of pk
in bytes (PK_BYTES_LEN
) is passed in. But what's confusing is that PK_BYTES_LEN
also happens to be the combined length (in elements) of the two adjacent pk
and sk
fields. Thus we end up discussing that even if two objects happen to be laid out contiguously in memory (which C may guarantee, but Rust does not), it's still UB to read off the end of one object into the data of another one (in both languages).
This means a digression into struct layouts in C and Rust as well as the C and Rust memory models' ideas of pointer provenance. These aren't bad things to discuss, but distract from the point of this slide, which is to describe the importance of fulfilling the contracts of unsafe functions.
I think we should remove sk
and the KeyPair
wrapper entirely and simply demonstrate a confused call to std::slice::from_raw_parts
. Then we can just say that the UB ultimately comes from reading outside the bounds of the object without having to discuss additional topics.