Optional Lab: Rust Memory Safety
Expect it to take about 30-60 minutes.
| Environment | Any machine with Rust 1.85 or newer (macOS, Linux, WSL, and Apple Silicon all work — no VM needed). The Rust Playground works for Parts A and C. |
| Files | rust-lab.tar.gz — a small Cargo project with three broken programs, a thread example, and a library with unit tests |
| Submission | One PDF, structured as described in Submitting a Lab Report |
| Points | 7 total — Part A: 3, Part B: 2, Part C: 2 |
Overview
Labs 1–3 attacked C programs: the bugs were a missing bounds check, a pointer that outlived its object, or user input that became a format string. In lecture we saw that most of these memory-corruption bugs become compile errors in Rust. The compiler tracks who owns each value and how long every reference lives, and it refuses to build a program in which a reference could outlive its data, alias a value that is being mutated, or be shared between threads unsafely.
In this lab you will trigger those errors yourself, read what the compiler is telling you, and fix the programs. Then you will write two small functions that handle untrusted input — an integer-overflow allocation size and the Heartbleed length field — without crashing, and finally make a multi-threaded counter compile and count correctly.
Topics covered:
- Ownership and moves, borrowing, and lifetimes
- How borrow-checker errors map to C bug classes (use-after-free, dangling pointers, iterator invalidation, …)
- Checked integer arithmetic and bounds-checked slicing on untrusted input
Send/Sync,Arc, andMutex: why data races are compile errors- What
unsafedoes — and does not — turn off
Background reading
- The Rust Programming Language (“the Book”), Chapter 4: Understanding Ownership and Chapter 16: Fearless Concurrency
rustc --explain <code>— every error code the compiler prints (e.g.E0382) comes with a long-form explanation and examples- CVE-2014-0160 (Heartbleed), for the background to Part B
Rules for every part
- No
unsafe. Every file starts with#![forbid(unsafe_code)]; leave it there. - No
.clone()in Part A. Copying the data makes the error go away without teaching you anything. - Keep each program’s printed output exactly as shown in the comment next to
its
println!. - Make the smallest change that works. A good fix is usually one or two lines.
Before you begin
Install Rust from https://rustup.rs if you do not have it, and check the version
(the project uses the 2024 edition, which needs 1.85 or newer; run rustup update
if yours is older):
$ rustc --version
Download the lab files and unpack them:
mkdir -p ~/cs487/rust-lab && cd ~/cs487/rust-lab
wget https://xiaoguang.wang/computer-security-labs/labs/files/rust-lab.tar.gz
# (no wget on macOS? use: curl -O <same URL>)
tar xzf rust-lab.tar.gz
cd Labsetup && ls
You should see:
Cargo.toml # the package manifest
src/lib.rs # Part B: two functions to implement, plus 6 unit tests
src/bin/a1_moved.rs # Part A1
src/bin/a2_invalidation.rs
src/bin/a3_dangling.rs
src/bin/c_counter.rs # Part C
Try building the first program:
$ cargo build --bin a1_moved
You should see a compile error. That is expected: Parts A and C start broken, and the error is the thing you are studying.
Note on
cargo test. Plaincargo testcompiles every program in the package, including the broken ones, so it fails until Parts A and C are fixed. While working on Part B, usecargo test --lib, which builds onlysrc/lib.rs.
Part A: Fixing the Borrow Checker
Each of the three programs below fails to compile. Each one is the Rust version of a classic C memory bug; in C it would compile without complaint and misbehave at run time.
| File | Run with |
|---|---|
src/bin/a1_moved.rs | cargo run --bin a1_moved |
src/bin/a2_invalidation.rs | cargo run --bin a2_invalidation |
src/bin/a3_dangling.rs | cargo run --bin a3_dangling |
For each program:
- Build it and read the whole error message, not just the first line. Rust
points at two places: where the problem starts (a move, a borrow, a scope
ending) and where it bites. Note the error code, e.g.
E0382. - Make the smallest change that makes it compile and print the expected output.
Respect the per-file constraints in the comments at the top (no
.clone(); ina3_dangling.rs, keeplongest_wordunchanged). - Decide which C bug class the error stopped: use-after-free, double free, dangling pointer (to a stack variable), iterator invalidation, …
Hint. For A1, ask who owns
wordsafter the call tototal_len, and whethertotal_lenactually needs ownership. For A2, think about whatpushmay do to the vector’s heap buffer, and whatbestpoints into. For A3, the reference is fine — the thing it refers to is not.
Deliverable (3 points — 1 per program). For each of the three programs:
- The error code and the compiler’s error message (paste it as text; the lines
with
^^^markers are enough). - Your fix, shown as the changed line(s) of code (before and after).
- The program’s output after the fix.
- The C bug class the error prevented, and two or three sentences explaining what would go wrong at run time if the equivalent C program ran. Be concrete: which memory is accessed after it is no longer valid, and why.
A summary table like the one below is a good way to start this section, but it does not replace the explanations.
| Program | Error code | Your fix (one line) | C bug it prevents |
|---|---|---|---|
a1_moved | |||
a2_invalidation | |||
a3_dangling |
Understanding check. In A2, the equivalent C++ program (
std::vector, a pointer or iterator to the max element, thenpush_back) often appears to work. Why does it sometimes work and sometimes print garbage, and why is “sometimes works” the dangerous case for security?
Part B: Handling Untrusted Input
Memory safety is not only about pointers. Two of the most famous C vulnerabilities came from trusting a number the attacker controlled:
- Integer overflow in an allocation size.
malloc(count * elem_size + HEADER)wraps around silently when the product is too large, so the program allocates a tiny buffer and then writescountelements into it — a heap overflow. - Heartbleed (CVE-2014-0160). An OpenSSL heartbeat message carried a length field and a payload. The server copied length bytes back to the client without checking that the payload was really that long, leaking up to 64 KB of server memory — private keys included — per request.
Open src/lib.rs and replace the two todo!()s:
alloc_size(count, elem_size)returnsSome(count * elem_size + HEADER), orNoneif any step overflows.parse_payload(packet)treats the first byte as a lengthnand returns then-byte payload that follows it, orNoneif the packet is empty or holds fewer thannbytes after the length byte. Bytes after the payload are ignored.
Neither function may panic on any input — a panic on attacker input is a denial-of-service bug, even if it is not memory corruption. Check your work with:
$ cargo test --lib
All 6 tests must pass.
Hint. Look at
checked_mulandchecked_addon integers, andsplit_firstandgeton slices. The?operator works onOptiontoo:x?returnsNonefrom the function early ifxisNone. Each function fits in one or two lines.
Deliverable (2 points).
1 point — working code:
- Your two function bodies, as text.
- The output of
cargo test --libshowing6 passed.
1 point — explanations:
- For
alloc_size: in the testalloc_size(usize::MAX / 4, 4), what value would the C expressioncount * elem_size + HEADERproduce on a 64-bit machine, and what would happen next if a programmalloced that size and wrotecountelements? - For
parse_payload: explain what the C Heartbleed code did with the input from the testpayload_heartbleed([200, 'b', 'y', 'e']) and what your Rust version does instead. - Suppose you had written
&rest[..n as usize]instead of usingget. What happens on the Heartbleed input? Explain why that is still memory-safe, and why it is still a bug for a server that handles untrusted packets.
Part C: Sharing State Between Threads
src/bin/c_counter.rs starts 8 threads that each increment one shared counter 1000
times. It uses Rc<RefCell<i32>> — the usual single-threaded way to share a mutable
value — and it does not compile.
$ cargo run --bin c_counter
Change the types so that it compiles and always prints 8000. Keep the 8
threads and the loop structure; you will need to change the use lines and a few
method calls to match the new types.
Hint. Look for the thread-safe counterpart of
Rcand the thread-safe counterpart ofRefCellinstd::sync. Read the error message carefully: it names the trait that is missing and the type that lacks it.
Run the fixed program several times to convince yourself the answer is always
8000.
Deliverable (2 points).
- The error code and the key lines of the compiler’s message.
- Your changed lines (before and after), and the output of a few runs.
- Why is
Rcrejected here? Say whatRcdoes internally that would go wrong if two threads did it at once. - Why is
RefCellnot enough, even inside anArc? Say whatRefCellchecks at run time and why that check itself is not safe across threads. - In one or two sentences: what would the equivalent C program (8
pthreads doingcounter++on a sharedintwith no lock) print, and why?
Final check
Before writing up, confirm that everything builds and passes together:
$ cargo build --bins && cargo test
Both commands must finish with no errors. Include this output in your report.
Cleaning up
cargo clean deletes the target/ build directory (a few hundred MB). You can delete the whole Labsetup/ folder once your report is submitted.
Submission
Submit a single PDF report. Do not upload your source files, a zip of the project, or a separate answers file — the report is the only thing that is graded, so everything must be in it:
CS487 Optional Lab: Rust Memory Safety — <Your Name>, <Your NetID>
1. Environment
- OS, and the output of `rustc --version`
2. Part A: Fixing the Borrow Checker
- Summary table, then A1, A2, A3: error, fix, output, explanation
3. Part B: Handling Untrusted Input
- Function bodies, test output, answers
4. Part C: Sharing State Between Threads
- Error, fix, output, answers
5. Final check
- Output of `cargo build --bins && cargo test`
Add screenshots of important code and execution outputs. Show only the lines you changed, not whole files. As in every lab, a fix with no explanation will not receive credit — the explanation of what the C version would have done is the point of the exercise.
See Submitting a Lab Report for general formatting guidance.