Memory safety explained
Memory safety is the state of being protected from various software bugs and security vulnerabilities when dealing with memory access, such as buffer overflows and dangling pointers.[1] For example, Java is said to be memory-safe because its runtime error detection checks array bounds and pointer dereferences. In contrast, C and C++ allow arbitrary pointer arithmetic with pointers implemented as direct memory addresses with no provision for bounds checking,[2] and thus are potentially memory-unsafe.[3]
History
Memory errors were first considered in the context of resource management (computing) and time-sharing systems, in an effort to avoid problems such as fork bombs.[4] Developments were mostly theoretical until the Morris worm, which exploited a buffer overflow in fingerd.[5] The field of computer security developed quickly thereafter, escalating with multitudes of new attacks such as the return-to-libc attack and defense techniques such as the non-executable stack[6] and address space layout randomization. Randomization prevents most buffer overflow attacks and requires the attacker to use heap spraying or other application-dependent methods to obtain addresses, although its adoption has been slow.[5] However, deployments of the technology are typically limited to randomizing libraries and the location of the stack.
Impact
In 2019, a Microsoft security engineer reported that 70% of all security vulnerabilities were caused by memory safety issues.[7] In 2020, a team at Google similarly reported that 70% of all "severe security bugs" in Chromium were caused by memory safety problems. Many other high-profile vulnerabilities and exploits in critical software have ultimately stemmed from a lack of memory safety, including Heartbleed[8] and a long-standing privilege escalation bug in sudo.[9] The pervasiveness and severity of vulnerabilities and exploits arising from memory safety issues have led several security researchers to describe identifying memory safety issues as "shooting fish in a barrel".[10]
Approaches
Most modern high-level programming languages are memory-safe by default, though not completely since they only check their own code and not the system they interact with. Automatic memory management in the form of garbage collection is the most common technique for preventing some of the memory safety problems, since it prevents common memory safety errors like use-after-free for all data allocated within the language runtime.[11] When combined with automatic bounds checking on all array accesses and no support for raw pointer arithmetic, garbage collected languages provide strong memory safety guarantees (though the guarantees may be weaker for low-level operations explicitly marked unsafe, such as use of a foreign function interface). However, the performance overhead of garbage collection makes these languages unsuitable for certain performance-critical applications.
For languages that use manual memory management, memory safety is not usually guaranteed by the runtime. Instead, memory safety properties must either be guaranteed by the compiler via static program analysis and automated theorem proving or carefully managed by the programmer at runtime. For example, the Rust programming language implements a borrow checker to ensure memory safety,[12] while C and C++ provide no memory safety guarantees. The substantial amount of software written in C and C++ has motivated the development of external static analysis tools like Coverity, which offers static memory analysis for C.[13]
DieHard,[14] its redesign DieHarder,[15] and the Allinea Distributed Debugging Tool are special heap allocators that allocate objects in their own random virtual memory page, allowing invalid reads and writes to be stopped and debugged at the exact instruction that causes them. Protection relies upon hardware memory protection and thus overhead is typically not substantial, although it can grow significantly if the program makes heavy use of allocation.[16] Randomization provides only probabilistic protection against memory errors, but can often be easily implemented in existing software by relinking the binary.
The memcheck tool of Valgrind uses an instruction set simulator and runs the compiled program in a memory-checking virtual machine, providing guaranteed detection of a subset of runtime memory errors. However, it typically slows the program down by a factor of 40,[17] and furthermore must be explicitly informed of custom memory allocators.[18] [19]
With access to the source code, libraries exist that collect and track legitimate values for pointers ("metadata") and check each pointer access against the metadata for validity, such as the Boehm garbage collector.[20] In general, memory safety can be safely assured using tracing garbage collection and the insertion of runtime checks on every memory access; this approach has overhead, but less than that of Valgrind. All garbage-collected languages take this approach.[1] For C and C++, many tools exist that perform a compile-time transformation of the code to do memory safety checks at runtime, such as CheckPointer[21] and AddressSanitizer which imposes an average slowdown factor of 2.[22]
BoundWarden is a new spatial memory enforcement approach that utilizes a combination of compile-time transformation and runtime concurrent monitoring techniques.[23]
Types of memory errors
Many different types of memory errors can occur:[24] [25]
- Access errors: invalid read/write of a pointer
- Buffer overflow – out-of-bound writes can corrupt the content of adjacent objects, or internal data (like bookkeeping information for the heap) or return addresses.
- Buffer over-read – out-of-bound reads can reveal sensitive data or help attackers bypass address space layout randomization.
- Invalid page fault – accessing a pointer outside the virtual memory space. A null pointer dereference will often cause an exception or program termination in most environments, but can cause corruption in operating system kernels or systems without memory protection, or when use of the null pointer involves a large or negative offset.
- Use after free – dereferencing a dangling pointer storing the address of an object that has been deleted.
- Uninitialized variables – a variable that has not been assigned a value is used. It may contain an undesired or, in some languages, a corrupt value.
- Null pointer dereference – dereferencing an invalid pointer or a pointer to memory that has not been allocated
- Wild pointers arise when a pointer is used prior to initialization to some known state. They show the same erratic behaviour as dangling pointers, though they are less likely to stay undetected.
- Memory leak – when memory usage is not tracked or is tracked incorrectly
- Stack exhaustion – occurs when a program runs out of stack space, typically because of too deep recursion. A guard page typically halts the program, preventing memory corruption, but functions with large stack frames may bypass the page.
- Heap exhaustion – the program tries to allocate more memory than the amount available. In some languages, this condition must be checked for manually after each allocation.
- Double free – repeated calls to free may prematurely free a new object at the same address. If the exact address has not been reused, other corruption may occur, especially in allocators that use free lists.
- Invalid free – passing an invalid address to free can corrupt the heap.
- Mismatched free – when multiple allocators are in use, attempting to free memory with a deallocation function of a different allocator[26]
- Unwanted aliasing – when the same memory location is allocated and modified twice for unrelated purposes.
Some lists may also include race conditions (concurrent reads/writes to shared memory) as being part of memory safety (e.g., for access control). The Rust programming language prevents many kinds of memory-based race conditions by default, because it ensures there is at most one writer or one or more readers. Many other programming languages, such as Java, do not automatically prevent memory-based race conditions, yet are still generally considered "memory safe" languages. Therefore, countering race conditions is generally not considered necessary for a language to be considered memory safe.
Notes and References
- Book: Dhurjati. Dinakar. Kowshik. Sumant. Adve. Vikram. Lattner. Chris. Proceedings of the 2003 ACM SIGPLAN conference on Language, compiler, and tool for embedded systems . Memory safety without runtime checks or garbage collection . 1 January 2003. 69–80. 10.1145/780732.780743. http://llvm.org/pubs/2003-05-05-LCTES03-CodeSafety.pdf. 13 March 2017. ACM. en. 1581136471. 1459540.
- Web site: Koenig. Andrew. How C Makes It Hard To Check Array Bounds. Dr. Dobb's. 13 March 2017.
- Akritidis. Periklis. Practical memory safety for C. Technical Report - University of Cambridge. Computer Laboratory. June 2011. 13 March 2017. University of Cambridge, Computer Laboratory. 1476-2986. UCAM-CL-TR-798.
- Anderson. James P.. Computer Security Planning Study. 2. Electronic Systems Center. ESD-TR-73-51.
- Book: van der Veen. Victor. dutt-Sharma. Nitish. Cavallaro. Lorenzo. Bos. Herbert. Research in Attacks, Intrusions, and Defenses . Memory Errors: The Past, the Present, and the Future . Lecture Notes in Computer Science . 7462. 86–106. 10.1007/978-3-642-33338-5_5. https://www.isg.rhul.ac.uk/sullivan/pubs/tr/technicalreport-ir-cs-73.pdf. 13 March 2017. 2012. 978-3-642-33337-8.
- Web site: Wojtczuk. Rafal. Defeating Solar Designer's Non-executable Stack Patch. insecure.org. 13 March 2017.
- Web site: Microsoft: 70 percent of all security bugs are memory safety issues . ZDNET . 21 September 2022 . en.
- Web site: CVE-2014-0160 . Common Vulnerabilities and Exposures . Mitre . en . 8 February 2018 . 24 January 2018 . https://web.archive.org/web/20180124041203/https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2014-0160 . live.
- Web site: Goodin . Dan . Serious flaw that lurked in sudo for 9 years hands over root privileges . Ars Technica . en-us . 4 February 2020.
- Web site: Fish in a Barrel . fishinabarrel.github.io . 21 September 2022.
- Web site: Crichton . Will . CS 242: Memory safety . stanford-cs242.github.io . 22 September 2022.
- Web site: References. The Rustonomicon. Rust.org. 13 March 2017. en.
- Bessey. Al. Engler. Dawson. Block. Ken. Chelf. Ben. Chou. Andy. Fulton. Bryan. Hallem. Seth. Henri-Gros. Charles. Kamsky. Asya. McPeak. Scott. A few billion lines of code later. Communications of the ACM. 1 February 2010. 53. 2. 66–75. 10.1145/1646353.1646374. 2611544 .
- Book: Berger. Emery D.. Zorn. Benjamin G.. Proceedings of the 27th ACM SIGPLAN Conference on Programming Language Design and Implementation . DieHard: Probabilistic memory safety for unsafe languages . 1 January 2006. 158–168. 10.1145/1133981.1134000. http://www.cs.umass.edu/~emery/pubs/fp014-berger.pdf. 14 March 2017. ACM. 1595933204 . 8984358. en.
- Book: Novark. Gene. Berger. Emery D.. Proceedings of the 17th ACM conference on Computer and communications security . DieHarder: Securing the heap . 1 January 2010. 573–584. 10.1145/1866307.1866371. https://people.cs.umass.edu/~emery/pubs/ccs03-novark.pdf. 14 March 2017. ACM. 9781450302456 . 7880497.
- Web site: Memory Debugging in Allinea DDT. dead. https://web.archive.org/web/20150203130856/https://www.allinea.com/user-guide/DDTMemoryDebugging.html. 2015-02-03.
- Web site: Gyllenhaal. John. Using Valgrind's Memcheck Tool to Find Memory Errors and Leaks. computing.llnl.gov. 13 March 2017. https://web.archive.org/web/20181107004639/https://computing.llnl.gov/code/memcheck/. 7 November 2018. dead.
- Web site: Memcheck: a memory error detector. Valgrind User Manual. valgrind.org. 13 March 2017. en.
- Web site: Kreinin. Yossi. Why custom allocators/pools are hard. Proper Fixation. 13 March 2017.
- Web site: Using the Garbage Collector as Leak Detector. www.hboehm.info. 14 March 2017. en-us.
- Web site: Semantic Designs: CheckPointer compared to other safety checking tools. www.semanticdesigns.com. Semantic Designs, Inc..
- Web site: AddressSanitizerPerformanceNumbers. GitHub.
- Dhumbumroong . Smith . 2020 . BoundWarden: Thread-enforced spatial memory safety through compile-time transformations . Science of Computer Programming . 198 . 102519. 10.1016/j.scico.2020.102519 . 224925197 .
- Web site: Gv. Naveen. How to Avoid, Find (and Fix) Memory Errors in your C/C++ Code. Cprogramming.com. 13 March 2017.
- Web site: CWE-633: Weaknesses that Affect Memory. Community Weakness Enumeration. MITRE. 13 March 2017. en.
- Web site: CWE-762: Mismatched Memory Management Routines. Community Weakness Enumeration. MITRE. 13 March 2017. en.