[RFC] Introduction of DoubleFreeSanitizer (DSan)

::Introduction::

Double-free bugs, where a program frees the same allocation more than once, are a common cause of heap corruption and security vulnerabilities in C and C++ programs.

AddressSanitizer already detects double-free bugs. However, ASan detects a much broader set of memory-safety errors by instrumenting memory accesses and maintaining shadow memory and redzones. This can impose significant execution-time and memory overhead. In particular, resource-constrained embedded environments may not have sufficient memory or runtime budget to run ASan at all. For projects that only need double-free detection, this cost can be unnecessary.

This RFC proposes DoubleFreeSanitizer (DSan), a standalone compiler-rt sanitizer focused on detecting double-free errors. DSan intercepts allocation and free APIs, tracks per-allocation state and stack traces, and reports an error when an allocation is freed again.

DSan does not require an LLVM IR instrumentation pass. It consists of a compiler-rt runtime and the minimal Clang driver support needed for the new -fsanitize=doublefree option.

::Naming::

The original intended name was DFSan, standing for “Double-Free Sanitizer.” However, DFSan is already widely used for DataFlowSanitizer, whose Clang option is -fsanitize=dataflow.

To avoid that naming conflict, this implementation currently uses DSan and the runtime name dsan.

I am open to renaming the sanitizer if the community prefers another name.

::Proposal::

The patch adds a new sanitizer option:

-fsanitize=doublefree

The option links the standalone DSan runtime. The runtime tracks allocation state in allocator metadata. Each allocation records:

  • whether the allocation is currently allocated;
  • whether it has already been freed;
  • the allocation stack trace;
  • the first free stack trace; and
  • the requested allocation size.

When the program frees a pointer that DSan has already observed as freed, DSan reports the error and terminates the program.

For example:

ERROR: DoubleFreeSanitizer: double-free on address 0x…

Second free (the invalid free) of address 0x…:
#0 free
#1 main

First free of address 0x…:
#0 free
#1 main

Original allocation of address 0x…:
#0 malloc
#1 main

SUMMARY: DoubleFreeSanitizer: double-free on address 0x…

::Implementation::

The implementation follows the structure and conventions of existing standalone compiler-rt sanitizers.

::Clang::

The patch adds DoubleFreeSanitizer to Sanitizers.def and introduces the -fsanitize=doublefree command-line option.

The Clang driver links the dsan runtime when this option is enabled. DSan is handled as a standalone runtime, similarly to LeakSanitizer, rather than requiring compiler-inserted instrumentation.

::compiler-rt::

The patch adds compiler-rt/lib/dsan, which provides:

  • a standalone allocator with per-chunk metadata;
  • interceptors for malloc, calloc, realloc, free, aligned allocation APIs, and related allocation functions;
  • interceptors for C++ allocation and free operators;
  • stack-trace collection for allocations and frees;
  • runtime flags and default-option support;
  • optional user hooks such as __dsan_default_options and __dsan_is_turned_off;
  • thread, TLS, and fork support based on sanitizer_common infrastructure; and
  • CMake and GN build integration.

::Tests::

The patch adds a compiler-rt/test/dsan test suite, including coverage for allocator behavior, runtime options, suppressions, C++ allocation/free operators, threading, TLS, fork behavior, Linux-specific behavior, and Darwin-specific behavior.

::Motivation::

DSan is intended for users who want focused double-free detection without enabling the complete AddressSanitizer feature set.

Potential use cases include:

  • CI configurations where ASan’s execution-time or memory overhead is too high;
  • Embedded devices that need to detect double-free errors but cannot run ASan because of resource constraints; This is the situation faced by my company.

::Limitations::

DSan intentionally focuses on double-free detection. It is not a replacement for AddressSanitizer.

In particular, DSan does not aim to detect:

  • heap-buffer-overflow;
  • stack-buffer-overflow;
  • use-after-free reads or writes;
  • use-after-return;
  • use-after-scope; or
  • other invalid memory accesses that ASan detects.

Platform support follows the platforms supported by the DSan runtime and corresponding Clang driver integration. The initial implementation includes support for the sanitizer platforms and architectures covered by the added compiler-rt and driver configuration.

::Future work::

Possible future directions include:

  • expanding platform and allocator coverage;
  • revisiting whether DSan should remain a distinct sanitizer or eventually share more implementation with an existing sanitizer runtime.

::Feedback requested::

I would appreciate feedback on:

  • whether a standalone double-free-only sanitizer is useful to the LLVM community;
  • the proposed DSan name, especially suggestions for a better name that does not conflict with DataFlowSanitizer;
  • expected sanitizer-option compatibility;

Assisted-by: GPT-5.6 Terra

Since gwp-asan has similar scope/interception, would it make sense to add DSan as another capability of gwp-asan, instead of an independent “sanitizer”? IIRC gwp-asan already has runtime options to dial up/down the frequency of checks, maybe this is another knob?

It seems to match the philosophy/goals of gwp-asan well IIUC.

Thank you for the opinion. I agree that GWP-ASan can overlap with this
proposal.

My understanding is that GWP-ASan samples allocation events themselves and
tracks only the selected allocations using metadata in its guarded pool.
Allocations that are not sampled have no GWP-ASan metadata.

In contrast, DSan needs to retain at least the allocation state and first-free
information for every allocation in order to detect double-free errors
deterministically. Implementing this in GWP-ASan would therefore require a
separate metadata mechanism for allocations outside the guarded pool. This
seems like a more fundamental change than adding another sampling or frequency
option.

AddressSanitizer already detects double-free bugs. […]

What stops you from linking the ASan runtime, but disable compiler instrumentation?

I disagree with adding narrow tools that are a subset of existing tools, where the usecase is extremely narrow and can be achieved by a number of other ways.

For example, you can use ASan without instrumentation which makes non-allocator runtime overheads zero:

% cat test.cc
__attribute__((noinline)) int *foo() {
  return new int;
}

__attribute__((noinline)) void bar(int *i) {
  *i += 1;
  delete i;
}

int main(int argc, char *argv[])
{
  auto x = foo();
  delete x;
  bar(x);
  return 0;
}

% clang++ -c test.cc -o test.o # no instrumentation, no globals redzones!
% clang++ -fsanitize=address -o test test.o && ./test |& head -4
=================================================================
==3456569==ERROR: AddressSanitizer: attempting double-free on 0x7bff839e0010 in thread T0:
    #0 0x5600c0f297b6 in operator delete(void*, unsigned long) (/usr/local/google/home/elver/temp/asan-light/test+0x1177b6) (BuildId: 7cfd21c6ef1985d44091f494a3135f79b934668b)
    #1 0x5600c0f29ff2 in bar(int*) (/usr/local/google/home/elver/temp/asan-light/test+0x117ff2) (BuildId: 7cfd21c6ef1985d44091f494a3135f79b934668b)

If you are running in a memory-constrained system, chances are it’s an Arm-based system, where we have HWASan (less memory used): -fsanitize=hwaddress (same trick as above works).

Furthermore, if you do not require integration with Clang code instrumentation, you are able to build an out-of-tree tool easily. The flag -fsanitize=doublefree is a convenience, but not required. Even then, there are tools that use Clang instrumentation and perform standard library interception that are maintained out of tree (e.g. see how GWPSan does it).

2 Likes

Granted, ARM is pretty popular but there’s plenty of other architectures used in memory-constrained environments. Do 32-bit ARM uCs even have the necessary features (MTE/TBI?).

So I think approaches like GWP-ASan and this new proposal are valuable to consider. But yeah let’s see if DSan could be a feature/mode of scudo which IIRC is the allocator GWP-ASan depends on? Probably LLVM libc has an allocator too? Maybe this would fit there.

HWASan needs TBI, so 32-bit won’t have that. But before speculating that it might be useful there, I’d want evidence that the instrumentation-less (normal) ASan doesn’t work in that environment. And at some point we have to draw the line what we want to support; too many similar variants or subvariants of a feature that compete with each other doesn’t help. If anything, we should figure out how to optimize or enhance what we have.

I still think creating a whole new sanitizer for this is overkill. It can either live out-of-tree, or be part of Scudo, as you suggest, if that has a lot of the pieces needed already.

1 Like

Thank you for the detailed feedback and the example. I wasn’t aware that it was possible to link the ASan runtime without compile-time instrumentation. I had overlooked that.

I generally agree with your opinion that we should avoid adding narrow tools that are mere subsets of existing sanitizers when the use case can be covered by other means. However, determining whether a use case is “extremely narrow” is often very difficult in practice.

In my case at LG Electronics, many of our TV products still operate with a 32-bit user space, even on 64-bit capable hardware. This means HWASan is not available as an option. Given the severe memory constraints (typically around 2GB total system memory shared with GPU, 4K video processing, Linux kernel, and always-on AI features like voice recognition), even the instrumentation-less ASan imposes too much overhead from shadow memory, quarantine, and allocator metadata.

I believed there was no other practical solution for this environment, which is why I proposed a dedicated lightweight DoubleFreeSanitizer.

Regarding the suggestion to integrate this as part of Scudo: I hadn’t seriously considered that option yet. Since Scudo is heavier than a ptmalloc as far as I know.

So, I will evaluate the real-world memory overhead of both instrumentation-less ASan and a potential Scudo-based implementation on LG Electronics TV, and follow up with concrete data. We can discuss the best integration path (standalone vs Scudo vs no integration vs out-of-tree) again once I have those numbers.

Thanks again for the opinions!