Skip to content

"Calling unsafe functions" example is confusingΒ #2733

Open
@fw-immunant

Description

@fw-immunant

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions