Lecture from: 14.10.2025 | Video: Videos ETHZ
This lecture contextualizes the course by examining the broader landscape of systems programming. Having explored C, a language offering direct control over hardware, the discussion now turns to the responsibilities and risks associated with such power. The lecture examines the systemic issues and common pitfalls of C, often metaphorically described as “juggling with chainsaws,” before introducing Rust, a modern alternative designed to provide similar low-level control with safety guarantees.
The Enduring Legacy and Perils of C
A Brief History of C
C was created in the early 1970s at Bell Labs by Dennis Ritchie and Ken Thompson during the development of the UNIX operating system. Its purpose was to serve as a systems programming language powerful enough for an OS kernel yet high-level enough for portability and productivity.
Decades later, C remains a dominant force:
- It is consistently one of the most used languages for open-source projects.
- The Linux kernel, projected for 2025, will contain around 40 million lines of C.
- The Android OS contains over 12 million lines of C.
- It is ubiquitous in embedded systems, including Wi-Fi routers, medical devices, vehicles, avionics, and military systems.
Why Do We Still Use C?
C’s longevity stems from features critical for systems programming:
- Layout Control: Unlike Java or C++, C provides precise control over data structure layout in memory.
- Predictable Performance: C constructs map closely to assembly language, avoiding hidden costs.
- Explicit Management: The programmer controls memory allocation and deallocation.
- Minimal Runtime: C relies minimally on a runtime system or libraries.
These features contribute to C’s dominance in mission-critical, security-critical, performance-critical, and concurrent domains.
The Uncomfortable Question
Given the prevalence of pointer errors, buffer overflows, and memory leaks, a fundamental question arises:
Should we really be using C for mission-critical, security-sensitive, concurrent code?
Data suggests a significant problem.
/Semester-3/Systems-Programming-and-Computer-Architecture/Lecture-Notes/attachments/Pasted-image-20251017140324.png)
The graph above illustrates the number of severe security vulnerabilities (CVEs) discovered in the Linux kernel annually. The rate of discovery in this production code is not decreasing.
/Semester-3/Systems-Programming-and-Computer-Architecture/Lecture-Notes/attachments/Pasted-image-20251017140333.png)
The implications are significant:
- Linux contains ~40 million lines of C.
- The bug discovery rate is roughly 0.5% per line of code per year.
- This suggests roughly 200,000 bugs are reported and fixed per year in production Linux code.
- It implies 1-2 million latent, undiscovered bugs likely exist in the Linux kernel today.
The Human Cost of Software Defects
These defects have real-world consequences.
WannaCry (2017)
A ransomware attack exploited a vulnerability in unpatched Windows versions, affecting hundreds of thousands of machines globally, including medical devices and MRI machines.
This highlights a difficult trade-off: upgrading mission-critical systems carries the risk of introducing new bugs (e.g., a software update causing a radiation therapy machine to malfunction), while failing to upgrade leaves systems vulnerable.
Book Plug: The Design of Everyday Things
Don Norman argues: If users make the same mistake repeatedly, it is the fault of the designer, not the user.
If programmers consistently make memory errors in C, the fault may lie in the language design. C is comparable to an airplane cockpit where the “raise landing gear” lever is identical to the “raise flaps” lever.
C Bugs in the Wild: A Gallery of Horrors
Several classic C vulnerability patterns exist.
The Ping of Death: A Buffer Overflow Attack
The Internet Protocol (IP) allows splitting large packets into fragments. The destination OS reassembles them using offset and length to copy contents into a buffer.
The “Ping of Death” attack (1996) exploited this by sending a malformed fragment with an offset + length exceeding the maximum packet size. The C-based kernel implementation failed to check bounds, overwriting critical kernel data structures and crashing the machine.
C and Undefined Behavior
The C standard leaves many behaviors “undefined”. A common example is pointer arithmetic wrap-around.
// A check to prevent buffer overflows
if (buf + len < buf)
return; // overflow, buf+len wrapped aroundA programmer might write this to detect wrap-around. However, since pointer wrap-around is undefined, an optimizing compiler may assume it never happens and silently remove the check, re-introducing the vulnerability.
Time of Check / Time of Use (TOCTOU)
This works as a race condition vulnerability. Consider an OS kernel function opening a file:
/* in the OS kernel */
filedesc fileopen(char *filename) {
if (!accessAllowed(filename)) // 1. Check permissions
return error;
return getFileHandle(filename); // 2. Use the filename
}/Semester-3/Systems-Programming-and-Computer-Architecture/Lecture-Notes/attachments/Pasted-image-20251017140812.png)
An attacker can exploit the gap between check and use:
- The attacker calls
fileopen("myFile"). - The kernel passes the
accessAllowedcheck. - A concurrent malicious thread overwrites
filenameto/etc/passwd. - The kernel executes
getFileHandleon the password file.
Fixing this in C is difficult, often requiring copies that introduce other risks.
/Semester-3/Systems-Programming-and-Computer-Architecture/Lecture-Notes/attachments/Pasted-image-20251017140908.png)
/Semester-3/Systems-Programming-and-Computer-Architecture/Lecture-Notes/attachments/Pasted-image-20251017140920.png)
Writing secure, correct C code is extraordinarily difficult due to the lack of a safety net.
Introduction to Rust
Rust is a modern systems programming language designed to address these issues.
What is Rust?
Rust is a modern systems programming language focusing on safety, speed, and concurrency. It accomplishes these goals by being memory safe without using garbage collection.
Ownership: Rust’s Secret Sauce
Ownership is the core concept enabling Rust’s safety. It is a set of compile-time rules.
The Ownership Rules
- Each value in Rust has a single owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped.
This provides deterministic deallocation managed by the compiler, distinct from garbage collection.
Values, Variables, and Pointers in Rust
- A value is data stored in a place (memory location).
- A variable is a named location on the stack.
- A pointer (or reference) holds the address of a place.
/Semester-3/Systems-Programming-and-Computer-Architecture/Lecture-Notes/attachments/Pasted-image-20251017140954.png)
Mutability
Variables in Rust are immutable by default. Explicit declaration with mut is required for change.
let num = 61; // Immutable
let mut year = 2024; // Mutable
year = 2025; // OK
num = 217; // COMPILE ERROR!Ownership and Moves
Assigning a variable owning heap data to another moves ownership.
fn main() {
// x 'owns' the heap-allocated string below
let x = String::from("D-INFK 61");
// Ownership of the string is "moved" from x to y.
let y = x;
// x no longer owns anything. It cannot be used.
println!("Hello, {}", x); // COMPILE ERROR!
}The compiler produces an error: borrow of moved value: 'x', preventing “use-after-free” bugs.
/Semester-3/Systems-Programming-and-Computer-Architecture/Lecture-Notes/attachments/Pasted-image-20251017141013.png)
The Copy Trait
Primitive types like integers implement the Copy trait, enabling bit-for-bit duplication rather than moves.
fn main() {
let x = 61; // x is an i32
let y = x; // This is a copy, not a move
println!("x is {}, y is {}", x, y); // This is OK!
}Borrowing: Access Without Ownership
Borrowing allows a function to use a value via a reference without taking ownership.
&T: Immutable (shared) reference.&mut T: Mutable (exclusive) reference.
fn double_value(val: &mut i32) { // Takes a mutable reference to an i32
*val = 2 * (*val); // Dereference and modify the original value
}
fn main() {
let mut x = 61;
double_value(&mut x); // Pass a mutable reference to x
println!("The new value is {}", x); // Prints 122
}The Borrowing Rules
The compiler enforces rules at compile time:
- Any number of immutable (shared) references are allowed simultaneously.
- Only one mutable (exclusive) reference is allowed at a time.
- Mutable and immutable references cannot coexist.
This prevents data races at compile time.
let mut x = Box::new(61);
let r1 = &x; // OK: immutable borrow
let r2 = &x; // OK: another immutable borrow
let r3 = &mut x; // COMPILE ERROR! Cannot borrow `x` as mutable because
// it is also borrowed as immutable.
println!("{}", r1); // The immutable borrow is used here.Lifetimes and the Borrow Checker
The borrow checker analyzes reference validity using non-lexical lifetimes (NLL).
/Semester-3/Systems-Programming-and-Computer-Architecture/Lecture-Notes/attachments/Pasted-image-20251017141041.png)
A borrow’s lifetime ends after its last use.
let mut x = Box::new(61);
let r1 = &x;
let r2 = &x;
println!("{}", r1); // Last use of r1 is here. Its borrow ends now.
// r2 was never used, so its borrow ends immediately.
let r3 = &mut x; // This is now OK, because the immutable borrows of r1 and r2
// are no longer active.Rust Memory Safety Summary
Rust’s ownership and borrowing system provides compile-time guarantees:
- No Data Races: Enforced by borrowing rules.
- No Null Pointers: Uses
Option<T>instead ofNULL. - No Use-After-Free: Enforced by ownership rules.
- No Out-of-Bounds Access: Runtime checks ensuring safe panics.
Rust achieves memory safety comparable to garbage-collected languages while maintaining C-like performance and control.
Practice: Rust Ownership and Borrowing
The “Borrow Checker” is the most famous part of Rust, and for good reason, it prevents entire classes of bugs.
Exercise: Move vs. Copy
What is the result of compiling this code?
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);
}Solution: Compile Error. String does not implement the Copy trait. Assigning s1 to s2 moves ownership. s1 is no longer valid.
Exercise: Borrowing Rules
Identify the error in this snippet:
let mut x = 42;
let r1 = &x;
let r2 = &mut x;
println!("{}", r1);Solution: Compile Error. You cannot have a mutable borrow (r2) while an immutable borrow (r1) is still active. The println at the end ensures r1 is still active when r2 is created.
Continue here: 10 X86 Architecture and Machine Level Programming