Memory Safety & Determinism
Memory safety and determinism are properties of how a program behaves, not just what it computes. Both matter enormously in systems, embedded, and security-sensitive programming, and both are explored far more deeply in the future Memory & Program Execution and Concurrency modules.
Memory safety means a program can never access memory it isn't supposed to — no reading past the end of an array, no using memory after it's been freed. Deterministic behavior means that running the same program with the same inputs always produces the same result, every single time.
Memory-unsafe code can corrupt unrelated data, crash unpredictably, or open a security hole an attacker can exploit. Nondeterministic behavior (when unintended) makes bugs incredibly hard to reproduce and fix — 'it worked on my machine but crashed randomly in production' is often a symptom of accidental nondeterminism or a hidden memory-safety violation.
Memory safety is a headline design goal of Rust, and is enforced automatically (at some performance/flexibility cost) by garbage-collected languages like Python, Java and JavaScript. C and C++ give the programmer full control and correspondingly full responsibility — memory-safety bugs in C/C++ are a leading cause of real-world security vulnerabilities.
Memory safety is like a library where every book has one exact assigned shelf, and you're physically prevented from grabbing a book off someone else's shelf by mistake. An unsafe system is a library with no such enforcement — nothing stops you from picking up (or even shredding) a book that belongs somewhere else, and the consequences only show up later when someone else goes looking for it.
C performs no automatic bounds checking. arr[10] doesn't raise an error — it reads whatever happens to sit in memory past the array, which could be garbage, another variable's data, or something that crashes the program much later, far from this line.
Memory safety violations include: buffer overflows (reading/writing past an array's bounds), use-after-free (accessing memory that's already been released), and dangling pointers (a pointer to memory that's no longer valid) — all covered in much more depth in the future Memory & Program Execution module.
Languages achieve memory safety through different mechanisms: automatic garbage collection with runtime bounds checks (Java, Python, JavaScript), or compile-time ownership/borrowing rules with zero runtime cost (Rust), or simply trusting the programmer entirely (C, C++, unless additional tools like sanitizers are used).
Determinism can be broken intentionally (a random-number generator, reading the current time) or accidentally (uninitialized variables, race conditions between threads, relying on undefined behavior, floating-point rounding differences across hardware).
'Undefined behavior' in languages like C and C++ is stronger than 'unspecified' — the language standard places no requirement whatsoever on what happens, meaning a compiler is legally allowed to produce any result at all, including ones that look nonsensical, for code that triggers it.
Assuming a memory-safety bug will always crash immediately and obviously — many corrupt memory silently and cause a failure much later, far from the actual bug, making them notoriously hard to trace.
Believing garbage-collected languages are entirely immune to memory issues — they eliminate use-after-free and buffer overflows in ordinary code, but memory leaks (holding references longer than needed) and excessive allocation are still very real problems.
Treating nondeterminism as always a bug — some nondeterminism is intentional and necessary (randomness for security, timestamps for logging); the problem is only when determinism was expected but not actually guaranteed.
Running memory sanitizers (AddressSanitizer, Valgrind) on C/C++ code during development to catch memory-safety violations before they reach production.
Choosing Rust specifically for new systems code where memory safety without a garbage collector's runtime cost is a priority.
Writing unit tests that seed random-number generators with a fixed value, specifically to make an otherwise-nondeterministic test reproducible.
Rust's ownership and borrowing model Rust enforces memory safety at compile time through a system where every value has exactly one owner, and references ('borrows') are checked by the compiler to guarantee they never outlive the data they point to, and that mutable and immutable borrows never coexist unsafely. This eliminates use-after-free and most data races without any garbage collector or runtime bounds-checking overhead — a major departure from both the 'manual and unsafe' (C/C++) and 'automatic but runtime-costly' (GC languages) approaches.
Undefined behavior as a compiler optimization license In C and C++, undefined behavior isn't just 'unpredictable at runtime' — compilers are permitted to assume undefined behavior never happens, and can optimize code accordingly. This means code that appears to work in an unoptimized build can behave completely differently (or be silently removed entirely) once optimizations are enabled, because the compiler assumed the undefined path was unreachable.
Determinism in concurrent and distributed systems Beyond single-threaded surprises like uninitialized reads, determinism becomes far harder to reason about with multiple threads or distributed processes: the exact interleaving of operations across threads (a race condition) or the exact order messages arrive across a network is generally not guaranteed, requiring explicit synchronization (covered fully in the Concurrency & Async module) to restore predictable behavior where it's needed.
What is a buffer overflow?
Writing (or reading) data past the allocated boundary of a buffer/array, into adjacent memory that belongs to something else — a classic source of crashes and, historically, a major class of security vulnerabilities.
How does Rust guarantee memory safety without garbage collection?
Through compile-time ownership and borrowing rules: every value has one owner, references are checked to never outlive their data, and the compiler statically rejects code that could cause a data race or use-after-free — all enforced before the program ever runs, with no runtime garbage collector needed.
Can a garbage-collected language still leak memory?
Yes. A memory leak in a GC language happens when a program keeps a reference alive (e.g. in a growing cache or an event-listener list) longer than intended, so the garbage collector never considers that memory unreachable and never reclaims it.
Why is undefined behavior in C considered dangerous even if the program 'seems to work'?
Because compilers are allowed to assume undefined behavior never occurs and optimize based on that assumption — code relying on it can behave correctly in one build and incorrectly (or unpredictably) in another, especially when optimization levels or compiler versions change.
int arr[5] = {1,2,3,4,5};
int x = arr[10]; // reading past the array's end — undefined behavior