We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 12 of 24
Validity invariants vs safety invariants
struct Rec { kind: u8, flag: bool, ch: char }
pub fn parse_records(bytes: Vec<u8>) -> Vec<i64>
parse_records decodes a byte stream into records. Each record is six
consecutive bytes:
| bytes | meaning | accepted when |
|---|---|---|
| 0 | kind, a u8 |
always (any value 0 to 255) |
| 1 | flag, to become a bool |
the byte is exactly 0 or 1 |
| 2 to 5 | a little-endian u32, to become a char |
the number is a Unicode scalar value (defined below) |
Process the complete records in input order and append three numbers to the output for each one:
- accepted record:
[kind, flag, scalar], whereflagis0or1andscalaris the decodedu32; - rejected record (a bad flag, a bad scalar, or both):
[-1, -1, -1].
The output therefore always has exactly three numbers per complete record. If the input length is not a multiple of six, the last one to five bytes are an incomplete record and produce no output. Empty input produces an empty output.
Example: the bytes [2, 1, 0x00, 0xF6, 0x01, 0x00] decode to kind 2, flag
1 and scalar 0x1F600 (128512, the 😀 emoji), so the output is
[2, 1, 128512]. Changing the second byte to 2 gives [-1, -1, -1].
The rule of the exercise is validate before constructing: check the raw
bytes first, and create a bool, a char or a Rec only from values that
passed. The rest of this page explains why checking a Rec after building
it from unchecked bytes is too late.
Two kinds of invariant
An invariant is a property that every value of a type is expected to have. Unsafe Rust distinguishes two kinds.
A type's validity invariant is set by the language, and the compiler relies on it everywhere. Producing a value that breaks it is undefined behavior (UB) immediately. In the Rust Reference's terms, a value is produced whenever it is assigned to or read from a place (a variable, field or memory location), passed to a function or operation, or returned from one. Nothing has to inspect the value afterwards for the program to have UB.
A type's safety invariant is set by the code that defines the type, and
its safe API relies on it. Examples: a Vec's first len elements are
initialized and len <= capacity; a str holds UTF-8. Breaking a safety
invariant is not in itself UB. The danger is what happens next: safe code
that relies on the invariant, such as indexing a Vec or iterating over a
str, can then cause UB. Code that lets a broken safety invariant reach such
safe code is unsound.
This leads to a practical rule. Unsafe code inside the module that owns a
type may break that type's safety invariant for a while, provided it is
restored before any code that relies on it can observe the value. A common
pattern is to write elements into a Vec's spare capacity and then call
Vec::set_len. The validity invariant has no such grace period: an invalid
bool exists the moment it is produced, however briefly it would have lived.
Check your understanding
Vec::<u8>::with_capacity(8) followed by set_len(8) before
Show answer
Hide answer
writing anything. Which invariant does that break?
It breaks Vec's safety invariant, and it breaks the documented
precondition of set_len, which requires the elements from the old length
to the new length to be initialized.
It does not by itself produce an invalid value. The Vec struct holds a
pointer, a length and a capacity, and all three are ordinary initialized
values. But every safe operation now trusts a length that is false. v[0]
reads an uninitialized u8, and an integer read from uninitialized memory is
an invalid value, so that read is UB. Because set_len is an unsafe fn,
the code that called it without meeting its precondition is the code at
fault.
For Vec<bool> the problem has the same shape, with one addition: even
initialized bytes would be invalid bool values unless each is 0 or 1.
Clippy's uninit_vec lint is in its correctness group, which is
deny-by-default, and it reports this exact pattern:
error: calling `set_len()` immediately after reserving a buffer creates uninitialized values
Common validity requirements
The Rust Reference lists these requirements. Its list is explicitly not exhaustive, and some entries are still under discussion, as noted below.
| type | requirement for a valid value |
|---|---|
bool |
the byte is 0 (false) or 1 (true) |
char |
at most char::MAX (0x10FFFF) and not a surrogate (0xD800 to 0xDFFF) |
! |
no valid value exists |
| integers, floats, raw pointers | any bit pattern, as long as it is initialized |
fn pointers |
non-null |
| structs, tuples, arrays | every field or element is valid |
| enums | a valid discriminant, and valid fields for the variant it names |
&T, &mut T, Box<T> |
aligned, non-null, not dangling; pointing at a valid T (this last part is still debated) |
wide pointers (&[T], &dyn Trait, raw wide pointers) |
the metadata matches the pointee: a correct vtable, or a slice length that keeps the total size at most isize::MAX for references and Box |
NonNull<T>, NonZero<T> |
a restricted range: never null, never zero |
Two definitions from the Reference help when reading the table.
Dangling means that not all of the bytes the pointer points to are part of the same live allocation. A pointer to zero bytes (for example to a zero-sized type) is never dangling, although a reference must still be non-null and aligned.
A place based on a misaligned pointer (a place reached through *ptr
where ptr is not aligned for its type) causes UB only when it is loaded
from or stored to. That is why &raw const on such a place is allowed while
& is not: & produces a reference, and a reference must be aligned. The
raw reference operator problem
works through this.
Why char needs a check
A char is four bytes and holds a Unicode scalar value: a number from
0 to 0x10FFFF that is not in the surrogate range 0xD800 to 0xDFFF.
Surrogates are not characters. In UTF-16, a character above 0xFFFF is
written as a pair of surrogate code units, so those numbers are reserved for
that encoding and are not assigned to characters.
So a u32 can fail to be a scalar value in two ways: it is above 0x10FFFF,
or it is a surrogate. char::from_u32(n) returns Option<char> and rejects
both. std::mem::transmute::<u32, char>(n) checks neither, and for a value
in either range it produces an invalid char, which is UB. The char
exists as soon as the transmute returns, so a check afterwards comes too
late.
Check your understanding
Why does char exclude surrogates instead of treating them as
Show answer
Hide answer
unusual characters?
Because they are not characters in Unicode, and UTF-8, the encoding str
uses, has no valid encoding for them. If a char could hold 0xD800, then
String::push(c) would have to write bytes that are not valid UTF-8,
breaking String's safety invariant. Since every char is a scalar value,
encoding one into a String never needs a check.
Validity invariants also let the compiler represent and optimize types more
tightly. Because bool has only two valid values, current rustc stores
Option<bool> in one byte, using an otherwise invalid byte value to mean
None. In the same way Option<char> is four bytes, and Option<NonNull<T>>
is guaranteed to be the size of a pointer. Code that
produces an invalid value breaks assumptions like these, so the result may
be far worse than one wrong number.
Related facts
std::mem::zeroed::<T>() returns a value whose bytes are all zero. That is
a valid u32, a valid bool (false) and a valid struct made only of such
fields. It is UB for &T, fn pointers, NonNull<T> and NonZero<T>,
because all-zero is not a valid value of those types. rustc's warn-by-default
invalid_value lint flags these cases when it can see the type, and it also
flags the deprecated std::mem::uninitialized::<T>() for both bool and
u32, because an uninitialized integer is invalid too.
The UTF-8 property of str is a safety invariant, not a validity one:
the Reference treats str like [u8], which only needs initialized bytes.
std::str::from_utf8_unchecked requires valid UTF-8 as its safety
precondition, so passing other bytes is still a bug that safe string
operations can turn into UB. rustc's deny-by-default
invalid_from_utf8_unchecked lint rejects the call when the bytes are a
visible constant.
Padding bytes, the gaps between a type's fields, do not need to be initialized. Together with unions, padding is the only place where reading uninitialized memory is allowed. The union and transmute problem builds on this.
What the tests cannot check
Undefined behavior does not reliably show up as a wrong answer. A version
that builds the Rec from unchecked bytes and checks it afterwards could
print the expected numbers for every test on one compiler version and fail
on another, so passing tests cannot show that code is free of UB. (Tools
such as Miri can detect many cases at run time, but they are not part of
this grader.)
The starter shows the problem. It builds Rec with two transmutes and then
checks only the upper bound of the scalar. On the current toolchain it fails
some tests, and it also fails linting: rustc's unnecessary_transmutes
lint flags the u32 to char transmute and Clippy's transmute_int_to_bool flags the u8 to
bool one. Those lints recognize the transmute calls; they do not detect the
UB itself.
A submission is graded on compiling, passing the tests, and producing no
warnings under cargo clippy -- -D warnings.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.