Memory lifetime tracking¶
Memory lifetime tracking determines the lifetimes of heap allocations made by a recorded program. These lifetimes can be queries using the uexperimental lifetime info command for a single pointer expression, or the uexperimental lifetime report command for all local variables. For each heap allocation, these commands report its address, its size, the time that it was allocated, and, if it was freed, the time it was freed. This can be used to investigate memory leaks and use-after-free bugs: you can ask whether a pointer refers to memory that is still valid, or list allocations that were never freed.
Memory lifetime tracking works by watching the program’s allocator functions, and replaying
execution history to reconstruct what was allocated and freed, and when. Use the
uexperimental lifetime function add command to
customize the set of allocator functions. If there are no custom allocators, the standard C library
functions malloc(), free(), calloc(), realloc(), and
reallocarray() are used.
Warning
Memory lifetime tracking is an experimental feature. The uexperimental lifetime
commands and their output may change without notice.
Querying lifetimes¶
uexperimental lifetime info expression¶
Report the memory lifetime status of expression, which is evaluated in the current frame and must evaluate to a pointer, smart pointer, or reference.
For example:
99% 7,513> uexperimental lifetime info ptr Auto-detecting standard allocator functions for lifetime tracking. ptr = (int *) 0x5555555592a0 "ptr" points to a valid memory location that was allocated at time 7,050:0x7ffff7e55650.
uexperimental lifetime report¶
Report the lifetime status of each pointer variable in the current frame. Each pointer is classified as pointing to allocated (alive) or freed (dead) heap memory, to a valid or invalid stack location, to static memory, or to unmapped memory.
For example:
99% 7,513> uexperimental lifetime report Memory report for "main" at time 7,513:0x55555555521f. Name Kind Type Value Status ------ ------ ------ -------------- ------------------------------------- ptr local int * 0x5555555592a0 alive (alloc at 7,050:0x7ffff7e55650)
uexperimental lifetime dump-all [-bt-depth N] filename¶
Write the lifetime of each heap allocation in execution history to filename, formatted in JSON. The data is grouped into allocations that are still alive, allocations that have been freed, and allocations that are in progress. Each entry includes the address, size and the time range during which the allocation was live.
- -bt-depth N¶
Also record, for each allocation, a backtrace of up to N frames captured at the point of allocation, and include it in the output. This may avoid the need to time-travel to the point of allocation.
Backtraces are experimental. They are gathered by walking the frame-pointer chain, so they are only meaningful for programs compiled with frame pointers (for example with the
-fno-omit-frame-pointeroption to gcc or clang), and frames are silently dropped where the chain cannot be followed.To generate the lifetimes, this command replays the whole of execution history, which is slow if history is long. The lifetimes are cached and reused in subsequent queries if possible.
For example:
99% 7,513> uexperimental lifetime dump-all lifetimes.json 99% 7,513> python import json 99% 7,513> python print(json.dumps(json.load(open("lifetimes.json")), indent=4)) { "alive": [ { "addr": 93824992252608, "len": 1024, "start": { "bbcount": 7311, "pc": 140737352390224 }, "end": null, "tid": 83 } ], "dead": [ { "addr": 93824992252576, "len": 4, "start": { "bbcount": 7050, "pc": 140737352390224 }, "end": { "bbcount": 7515, "pc": 140737352391984 }, "tid": 83 } ] }
Custom allocators¶
If your program uses its own allocation functions, register them using the uexperimental lifetime function add command, so that the lifetimes of objects allocated using these functions can be tracked.
Note that changing the set of custom allocator functions discards the cached lifetimes, if any, so that the next query command must regenerate the lifetimes.
uexperimental lifetime function auto-detect¶
Detect and register the standard C library allocation functions (
malloc(),free(),calloc(),realloc(), andreallocarray()).
uexperimental lifetime function add [options] function¶
Register function as a custom allocator. Use the options to describe which argument carries each relevant value. For example,
uexperimental lifetime function add -size-arg 0 my_malloc.
- -size-arg INDEX, -s INDEX¶
The index of the allocation size argument, for a function that behaves like
malloc(),calloc(),realloc(), orreallocarray().
- -free-arg INDEX, -f INDEX¶
The index of the pointer argument, for a function that behaves like
free(),realloc(), orreallocarray().
- -count-arg INDEX, -c INDEX¶
The index of the element count argument, for function that behaves like
calloc()orreallocarray(). The total allocated size is the element count parameter multiplied by the size parameter.
uexperimental lifetime function list¶
List the custom allocator functions registered by the uexperimental lifetime function add command.
uexperimental lifetime function remove function¶
Remove function from the list of custom allocator functions.
uexperimental lifetime function save filename¶
Save the custom allocator functions to filename. They can later be restored using the uexperimental lifetime function load command.
uexperimental lifetime function load filename¶
Load the custom allocator functions from filename, which must have previously been created using the uexperimental lifetime function save command.
Comparison with Address Sanitizer and Valgrind¶
The Address Sanitizer (ASan) and Valgrind’s Memcheck tools also find memory errors, but work in a different way. ASan and Valgrind watch the program as it runs and report an error at the instant memory is misused, while memory lifetime tracking does not observe the live program at all. It reconstructs the lifespan of each allocation by replaying execution history.
Where lifetime tracking does better¶
Analysis of a captured incident.
The recording is made without any memory tooling in place, and you decide to investigate lifetimes afterwards. With ASan you must recompile and rerun, and with Valgrind you must rerun under the tool. Both require you to reproduce the failure under the tool, which can often be the hard part for memory bugs. A recording captures the one run that went wrong and you can analyze it as many times as you like, along with the time-travel capabilities of UDB, allowing for an easy investigation and root cause analysis.
Questions about arbitrary points in time, not just the moment of the crash.
uexperimental lifetime info ptranswers whether a pointer was valid at the current time in the recording, and you can move to any time and ask again. A single lifetime generation pass shows both leak information and use-after-free information. ASan and Valgrind report at the moment of misuse and cannot look back.
No source changes or recompilation.
Allocators are matched by symbol, so tracking works on any ordinary build. In addition, custom allocators can be registered by name with uexperimental lifetime function add.
Where ASan and Valgrind do better¶
Buffer overflows and out-of-bounds access.
ASan’s red zones catch reads and writes just past the end (or before the start) of an allocation, and Valgrind detects many invalid accesses too. Lifetime tracking records the exact address range of each allocation but does not check its surroundings, so it does not flag an overflow that stays within mapped memory.
Uninitialized memory.
Valgrind’s Memcheck tracks definedness at the bit level and reports reads of uninitialized data. Lifetime tracking has no equivalent: it knows when memory was allocated and freed, not whether it was written before being read.
Reliable allocation backtraces.
ASan and Valgrind attach a dependable backtrace to every allocation and free. Lifetime backtraces are experimental, are gathered by walking the frame-pointer chain, and are therefore only meaningful when the program was compiled with frame pointers (see the
-bt-depthoption to the uexperimental lifetime dump-all command).
Immediate, unconfigured allocator coverage.
ASan and Valgrind understand the C runtime’s allocators out of the box, including many inlined or internal paths. Lifetime tracking follows the functions it has been told about (the standard allocators by default), so unusual or inlined allocators may need manual registering.
In short, ASan and Valgrind are strongest when you can readily reproduce a failure and want the widest catalog of error kinds flagged the moment they happen. Memory lifetime tracking is strongest when the failure is already recorded, especially if it is flaky or came from production, and you want to reason about what was valid when, with the whole execution available to explore.