memtrace
PlaygroundBetaLearn memoryFollow on

Getting started

  • Introduction

Unsafe Rust

  • Introduction
  • Raw pointers
  • Undefined behavior

Real world

  • Cloudflare: 100 TB saved in 1.1.1.1's DNS cache

Playground

  • How the Playground works
  • What the Playground can't do yet

Playground

What the Playground can't do yet

The programs the engine can't step through, why, and what to write instead

In short

The Playground runs a teaching-sized subset of Rust: one file, the standard library only, no crates. When a program falls outside that, it says so on the right, naming what stopped it and what to try instead, rather than drawing something wrong

Desktop only

It needs a screen at least 1024 px wide, a laptop or desktop. Phones and tablets get a note pointing to the visualizers, which work everywhere

What works

Most single-file Rust a learner writes. Before the gaps, here's what the Playground steps through and draws, each checked against a native rustc build:

  • Ownership: moves, clone(), functions taking and returning owned values, shadowing, reassigning a moved variable, Drop impls and the order they run in
  • Borrowing: & and &mut, references held in structs, slices of a Vec or String, methods taking &self, &mut self and self, closures that borrow
  • The heap: Box, String, Vec (growth, shrink_to_fit, remove, insert, retain, drain, split_off, clear), nested collections, a Box linked list, HashMap and HashSet, mem::take, mem::replace and mem::swap
  • Shared ownership: Rc and Arc with their counts, Weak in one direction, Cell and RefCell
  • Static memory: string literals and static items, from the first step
  • Unsafe Rust: raw pointers into the stack and the heap, Box::into_raw and from_raw, ptr::read, write, swap and copy_nonoverlapping, slice::from_raw_parts and from_raw_parts_mut, Vec::from_raw_parts and String::from_raw_parts, ManuallyDrop, NonNull, std::alloc, transmute, read_unaligned, unsafe fn, and leaks
  • Byte-level code: to_le_bytes and from_le_bytes, copy_from_slice, split_at_mut, try_into, account-style data in a Vec<u8>, zero-copy casts to a #[repr(C)] struct, size_of and align_of, arrays like [u8; 32]
  • Arithmetic: checked_*, wrapping_* and saturating_*, truncating as casts, try_from, and overflow, which panics as it does in a debug build
  • Panics: overflow, an index out of bounds, unwrap on None, a second borrow_mut, panic!. The walkthrough shows the line that panicked with its message, then the stack unwinding and dropping what it owned
  • Undefined behavior Miri catches: use after free, double free, reads past the end of a block, pointers into a function that already returned, a raw pointer kept across a Vec reallocation
  • Everyday code: structs, enums, Option and Result, generics, traits without dyn, recursion, loops, let … else, ?, closures, macro_rules!, format!, derived Debug, Clone and PartialEq, and the code before a thread::spawn
  • {:?} on your own types, and on the Vec, Option, Result, Box and tuples they sit in

Programs that won't run

Types the engine can't read yet:

  • Trait objects. A dyn Trait value can't be read. Use the concrete type, or a generic
  • Slices of arrays, like &arr[..], a &[i32] local, or a Box<[i32]> from into_boxed_slice. Slices of a Vec work, so keep the numbers in a Vec
  • MaybeUninit, initialized or not. Use a plain value, or an Option while it isn't set
  • A reference into the data of an enum variant: if let Some(x) = &opt, match &ix { Ix::Deposit { amount } => … }, a &self method that matches its own variants, or == between values inside an Option or Result of your own type. The engine can't place such a reference yet. A reference to the whole enum works, and so does an Option of a Box, String or Vec. Match by value instead: on the enum, a copy, or a clone
  • Unions. Reinterpret bits with to_bits and from_bits, to_le_bytes, or transmute

Patterns that crash it:

  • Values that point at each other in a cycle, through Rc or Weak: a child that knows its parent, say. The engine follows the pointers round and round. Links in one direction work
  • Some string literals. let name = "text"; and passing one to a function work. A literal stored in a struct field, in a vec![…], used in a match arm, or returned from a function doesn't. Use a String there
  • Hand-written Debug or Display impls. Any function with a named Formatter crashes the engine. #[derive(Debug)] works; a fmt you write yourself doesn't
  • A misaligned raw pointer read, like a u64 read at an odd offset of a byte buffer. Rust's debug checks abort the program on the spot, before there's anything to draw, and the Playground says so. Use ptr::read_unaligned, or from_le_bytes
  • Loops that never end. A run is stopped after 20 seconds
1// Instead of a slice of an array…
2let arr = [1, 2, 3];
3let s: &[i32] = &arr;
4
5// …slice a Vec
6let numbers = vec![1, 2, 3];
7let first_two = &numbers[..2];
The usual workaround: the same idea, with types the engine can read

Programs that stop early

  • Threads. The engine follows one thread at a time, so the walkthrough runs up to the first thread::spawn and stops there, with a step saying why. Everything before it is shown as it ran. Code that waits for its threads, like thread::scope, stops the engine itself, so it shows a note instead: run each thread's code in main, one after the other
  • Undefined behavior. Miri stops the program at the first one it sees, and the last step says which kind it was
  • Panics. The walkthrough runs to the line that panicked, says so with Rust's message, and shows the stack unwinding. The program then exits with an error, as it would
  • Pointers to unaligned data, like addr_of! on a field of a #[repr(packed)] struct. Reading it with read_unaligned is fine, but the engine can't follow such a pointer, so the walkthrough stops there and says why

Drawn simpler than real memory

  • Addresses. {:p} prints addresses from Miri's simulated memory. The addresses in the diagram are made up to read easily. Both are consistent on their own, but they don't match each other
  • Loops. The engine pauses only at the statements of the function itself, so a whole for or while loop, and everything inside it, shows as one step with its final values. The same goes for an if and for a block inside a loop
  • Some blocks. An unsafe { … } or { … } block directly in a function is split into a step per statement. One that declares a name also used outside it (shadowing, say), defines items inside, or hands back a value stays one step
  • Statics. Only static items with a literal value are drawn, and static mut isn't: the engine doesn't trace statics, so their changes can't be shown
  • HashMaps. A HashMap or HashSet shows how many entries it holds and its table on the heap, but not the keys and values inside: to the engine, the table is raw bytes
  • Leaks. The engine only sees heap blocks the program can still reach, so a freed block and a leaked one both just vanish from its trace. The Playground works out which from what pointed at the block last: an owner means freed; only a raw pointer or a reference, or mem::forget, means leaked
  • std's machinery. Internals are drawn by what they are: a RefCell as its value, an atomic as its number, a thread's JoinHandle and Thread by name
  • Other output. Only println!, print!, eprintln! and eprint! reach the terminal. dbg! and writing to io::stdout() directly don't
  • Aliasing bugs. This build of Miri doesn't check Rust's aliasing rules (Stacked Borrows). Writing through a raw pointer after taking a &mut to the same data isn't flagged, though full Miri would flag it. The memory drawn is still right. Every unsafe example in these docs and the Playground's tests is also checked with full Miri
  • Panic locations. The terminal shows thread 'main' panicked at line 5: where a real run gives the file and column, src/main.rs:5:13:

No undefined behavior step doesn't mean no undefined behavior

When the Playground stops with undefined behavior, it's real: Miri saw it happen. But a clean run only means Miri saw nothing on that one run, with these checks. For unsafe code you care about, run the full Miri too: cargo +nightly miri run

Why these limits

Most of them are gaps in Aquascope itself, which is still a research project. It finds the place a reference points to by walking the type's layout, and that walk doesn't handle enum variants or slices of arrays yet. The Playground works around what it safely can (printing, string literals, derived Debug, leaks, threads, blocks, panics, struct layout) and names the rest clearly. As Aquascope improves, the engine can be updated and some of these will go away

Resources

  • Aquascope issuesKnown gaps in the engine, such as #75 for `&'static str`
  • Stacked BorrowsThe aliasing model full Miri checks and this engine doesn't
PreviousHow the Playground works

On this page

  1. In short
  2. What works
  3. Programs that won't run
  4. Programs that stop early
  5. Drawn simpler than real memory
  6. Why these limits
  7. Resources