Scudo is a dynamic user-mode memory allocator, or heap allocator, designed to be resilient against heap-related vulnerabilities (such as heap-based buffer overflow, use after free, and double free) while maintaining performance. It provides the standard C allocation and deallocation primitives (such as malloc and free), as well as the C++ primitives (such as new and delete).
Scudo is more of a mitigation than a fully fledged memory error detector like AddressSanitizer (ASan).
Since the Android 11 release, scudo is used for all native code (except on low-memory devices, where jemalloc is still used). At runtime, all native heap allocations and deallocations are serviced by Scudo for all executables and their library dependencies, and the process is aborted if a corruption or suspicious behavior is detected in the heap.
Scudo is open source and part of LLVM's compiler-rt project. Documentation is available at https://llvm.org/docs/ScudoHardenedAllocator.html. The Scudo runtime ships as part of the Android toolchain and support was added to Soong and Make to allow for easy enabling of the allocator in a binary.
You can enable or disable extra mitigation within the allocator using the options described below.
Customization
Some parameters of the allocator can be defined on a per-process basis through several ways:
- Statically: Define a
__scudo_default_options
function in the program that returns the options string to be parsed. This function must have the following prototype:extern "C" const char *__scudo_default_options()
. - Dynamically: Use the environment variable
SCUDO_OPTIONS
containing the options string to be parsed. Options defined this way override any definition made through__scudo_default_options
.
The following options are available.
Option | 64-bit default | 32-bit default | Description |
---|---|---|---|
QuarantineSizeKb |
256 |
64 |
The size (in KB) of quarantine used to delay the actual deallocation of
chunks. A lower value may reduce memory usage but decrease the effectiveness
of the mitigation; a negative value falls back to the defaults. Setting
both this and ThreadLocalQuarantineSizeKb to zero disables the
quarantine entirely. |
QuarantineChunksUpToSize |
2048 |
512 |
The size (in bytes) up to which chunks can be quarantined. |
ThreadLocalQuarantineSizeKb |
64 |
16 |
The size (in KB) of per-thread cache use to offload the global quarantine.
A lower value may reduce memory usage but might increase contention on the
global quarantine. Setting both this and QuarantineSizeKb to zero
disables the quarantine entirely. |
DeallocationTypeMismatch |
false |
false |
Enables error reporting on malloc/delete, new/free, new/delete[] |
DeleteSizeMismatch |
true |
true |
Enables error reporting on mismatch between sizes of new and delete. |
ZeroContents |
false |
false |
Enables zero chunk contents on allocation and deallocation. |
allocator_may_return_null |
false |
false |
Specifies that the allocator can return null when a recoverable error occurs, instead of terminating the process. |
hard_rss_limit_mb |
0 |
0 |
When the process's RSS reaches this limit, the process terminates. |
soft_rss_limit_mb |
0 |
0 |
When the process's RSS reaches this limit, further allocations fail or
return null (depending on the value of allocator_may_return_null ), until
the RSS goes back down to allow for new allocations. |
allocator_release_to_os_interval_ms |
N/A | 5000 |
Only affects a 64-bit allocator. If set, tries to release unused memory to the OS, but not more often than this interval (in milliseconds). If the value is negative, memory isn't released to the OS. |
abort_on_error |
true |
true |
If set, the tool calls abort() instead of _exit()
after printing the error message. |
Validation
Currently, there are no CTS tests specifically for Scudo. Instead, make sure that CTS tests pass with or without Scudo enabled for a given binary to verify that it doesn't impact the device.
Troubleshooting
If a non-recoverable issue is detected, the allocator
displays an error message to the standard error descriptor and then terminates the process.
Stack traces that lead to the termination are added in the system log.
The output usually starts with Scudo ERROR:
followed by a
short summary of the problem along with any pointers.
Here is a list of the current error messages and their potential causes:
corrupted chunk header
: The checksum verification of the chunk header has failed. This is likely due to one of two things: the header was overwritten (partially or totally), or the pointer passed to the function is not a chunk.race on chunk header
: Two different threads are attempting to manipulate the same header at the same time. This is usually symptomatic of a race-condition or general lack of locking when performing operations on that chunk.invalid chunk state
: The chunk isn't in the expected state for a given operation, for example, it's not allocated when trying to free it, or it's not quarantined when trying to recycle it. A double free is the typical reason for this error.misaligned pointer
: Basic alignment requirements are strongly enforced: 8 bytes on 32-bit platforms and 16 bytes on 64-bit platforms. If a pointer passed to our functions does not fit those, the pointer passed to one of the functions is out of alignment.allocation type mismatch
: When this option is enabled, a deallocation function called on a chunk has to match the type of function that was called to allocate it. This type of mismatch can introduce security issues.invalid sized delete
: When the C++14 sized delete operator is used, and the optional check is enabled, there's a mismatch between the size that was passed when deallocating a chunk and the size that was requested when allocating it. This is typically a compiler issue or a type confusion on the object being deallocated.RSS limit exhausted
: The maximum RSS optionally specified has been exceeded.
If you're debugging a crash in the OS itself, you can use a HWASan OS build. If you're debugging a crash in an app, it's possible to use a HWASan app build too.