Sanitize
Adds Ceedling tasks to build and run test executables with the compiler’s sanitizers. AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan), and LeakSanitizer (LSan) find memory errors, undefined behavior, and leaks while your tests run.
The Sanitize plugin creates a duplicate test build with sanitize: command
line plugin tasks. This is intentional and needed.
Note
A sanitize: task is a delta build.
This plugin maintains its own dependency cache. A test executable will be
rebuilt the first time you run a sanitize: build.
Plugin overview
Linux and macOS
The sanitizers run on Linux with GCC or Clang and on macOS with Clang. Leak detection is Linux only. The plugin is not supported on Windows.
Enabling this plugin on a platform without sanitizers does not affect your
other builds. Only a sanitize: task requires them.
The Sanitize plugin compiles and links your tests with the sanitizers, then runs each test executable with the sanitizers’ runtime options set for it alone. Each sanitizer reports what it finds as the test runs.
The plugin provides invocation modes mirroring standard test: tasks.
- Whole test suite:
ceedling sanitize:allbuilds and runs every test executable with sanitizers. - Single test file:
ceedling sanitize:<filename>builds and runs one.
By default a sanitizer stops a test at its first finding, and any finding fails the build. Each test’s sanitizer reports are saved to a log file of its own.
Sanitizer flags only reach the builds this plugin instruments. Builds from
other plugins, such as gcov: and valgrind:, are never instrumented.
Set up
Toolchain
The sanitizers are part of GCC and Clang. No separate installation is needed where your compiler provides them.
- Linux: GCC or Clang with their sanitizer runtimes. Some distributions
package these separately (e.g.
libasanandlibubsan). - macOS: The Clang that ships with Xcode or the Command Line Tools.
The plugin runs each test executable through env, which sets the sanitizers’
runtime options. env is standard on Linux and macOS.
The -plugins variants of the MadScienceLab Docker images
come with a ready toolchain.
Enable the plugin
Enable the plugin by adding sanitize to the enabled plugins list in your
project configuration:
Configuration
The plugin’s defaults turn on ASan and UBSan, stop a test at its first finding,
check for leaks where supported, and fail the build on any finding. Change any
of these under an optional :sanitize section in your project configuration.
Default configuration
:sanitize:
:sanitizers:
- :address
- :undefined
:recover: FALSE
:detect_leaks: TRUE
:test_builds: FALSE
:fail_build: TRUE
:reports: []
:runtime_options:
:asan: []
:ubsan: []
:lsan: []
:flags:
:compile: []
:link: []
:sanitizers:
The sanitizers to build with. Any of :address, :undefined, and :leak.
Names written as strings, such as address, work too.
ASan’s runtime includes leak detection, so :leak adds nothing beside
:address. List :leak alone for leak detection without ASan.
Default: [:address, :undefined]
:recover:
When false (the default), a sanitizer stops a test at its first finding.
When true, the test keeps running and each finding is reported.
Default: false
:detect_leaks:
When true (the default), ASan checks for leaks as each test executable exits.
Leak detection is not supported on macOS. There, the plugin disables it with a
notice.
Default: true
:test_builds:
When true, ordinary test: builds are built with the sanitizers too. A
sanitize: build is always instrumented.
Default: false
:fail_build:
When true (the default), any sanitizer finding fails the build. When false,
findings are still logged and saved, but the build is not failed for them.
A test a sanitizer stopped is still a failing test either way.
Default: true
:reports:
Reports of a build’s findings, written for each instrumented build. Any of
:text, :json, and :sarif.
:textcollects each finding’s report as the sanitizer printed it.:jsonlists each finding’s test, sanitizer, kind, file, line, and message.:sarifwrites SARIF 2.1.0 for code scanning services such as GitHub code scanning. Each finding is an error.
The JSON and SARIF reports are written even for a build without findings, so a CI step always finds its file. An empty SARIF run tells a code scanning service that earlier findings are gone.
Default: [] (the text report alone)
:runtime_options:
Extra runtime options for each sanitizer, appended to the plugin’s own. Each
entry is one name=value option.
The plugin sets these options itself:
| Variable | Options |
|---|---|
ASAN_OPTIONS |
halt_on_error, exitcode=0, and detect_leaks (Linux) |
UBSAN_OPTIONS |
halt_on_error, print_stacktrace=1, and exitcode=0 |
LSAN_OPTIONS |
exitcode=0, when the leak sanitizer runs without ASan |
exitcode=0 lets the plugin judge each run by what the sanitizers report rather
than by their exit codes. See How findings are reported.
Default: none
:flags:
Extra compile and link flags for instrumented builds. They follow the plugin’s
own -fsanitize, -fno-omit-frame-pointer, and -g, plus
-fno-sanitize-recover=all unless :recover: is on.
Default: none
Flags and defines for the sanitize: build
A sanitize: build is a build context of its own. Like :test, it accepts
:flags and
:defines entries, matchers included.
Without its own entries for an operation, a sanitize: build uses :test’s.
Usage
The Sanitize plugin follows the conventions of test: tasks.
Run sanitizers for all tests
Run sanitizers for a single test file
Pass the test filename, no path:
You can also pass just the source filename. Ceedling locates the corresponding test file:
How findings are reported
A sanitizer’s finding is reported where you would look for a test failure.
- A halted test fails, named by its finding. When a sanitizer stops a test, the test executable ends before Unity prints its summary, and Ceedling treats that as a crash. The crash notice names the sanitizer, the kind of finding, its location, and the test’s log. It replaces Ceedling’s generic crash notice and its backtrace advice:
☠️ ERROR: Test executable `test_buffer.out` was stopped by a sanitizer finding.
> AddressSanitizer heap-buffer-overflow at test/test_buffer.c:21
> Boom log: build/artifacts/sanitize/sanitize/test_buffer.log
The test’s failure message carries the same on one line:
AddressSanitizer heap-buffer-overflow at test/test_buffer.c:21 (Boom log: build/artifacts/sanitize/sanitize/test_buffer.log)
-
A leak is a finding, not a test failure. Leaks are found as a test executable exits, after every test case has reported. Your test results stay as Unity reported them, and the leak is logged and counted.
-
A crash no sanitizer reported keeps Ceedling’s usual crash message.
Each finding is also logged as the test runs, except one a crash notice
names. With :fail_build: enabled, the build failure summary
counts the findings and names the reports written:
• [SANITIZE] Sanitizers found 2 problems in 2 tests. See the reports in build/artifacts/sanitize/sanitize: sanitize.log, sanitize.json.
Example console output
These excerpts come from real runs. Build progress lines are left out.
A halted test, with :reports set to [:text, :json, :sarif]:
☠️ ERROR: Test executable `test_nosanitize_heap_buffer_overflow.out` was stopped by a sanitizer finding.
> AddressSanitizer heap-buffer-overflow at test/test_nosanitize_heap_buffer_overflow.c:29
> Boom log: build/artifacts/sanitize/sanitize/test_nosanitize_heap_buffer_overflow.log
SANITIZE: FAILED TEST SUMMARY
[test/test_nosanitize_heap_buffer_overflow.c]
Test: test_writes_past_its_buffer
At line (27): "AddressSanitizer heap-buffer-overflow at test/test_nosanitize_heap_buffer_overflow.c:29 (Boom log: build/artifacts/sanitize/sanitize/test_nosanitize_heap_buffer_overflow.log)"
SANITIZE: ❌ OVERALL TEST SUMMARY
TESTED: 2
PASSED: 1
FAILED: 1
IGNORED: 0
❌ BUILD FAILURE SUMMARY
• [SANITIZE] Unit test failures
• [SANITIZE] Sanitizers found 1 problem in 1 test. See the reports in build/artifacts/sanitize/sanitize: sanitize.log, sanitize.json, sanitize.sarif.
A leak. Every test passes, and the leak alone fails the build. The test’s own output section shows the sanitizer’s report:
🪲 ERROR: test_nosanitize_leak: LeakSanitizer memory-leak at test/test_nosanitize_leak.c:32 ➡️ see build/artifacts/sanitize/sanitize/test_nosanitize_leak.log
SANITIZE: TEST OUTPUT
[test/test_nosanitize_leak.c]
- "==456==ERROR: LeakSanitizer: detected memory leaks"
- "Direct leak of 32 byte(s) in 1 object(s) allocated from:"
- " #1 0x55f0b16d72e9 in test_loses_its_allocation test/test_nosanitize_leak.c:32"
- "SUMMARY: AddressSanitizer: 32 byte(s) leaked in 1 allocation(s)."
SANITIZE: ✅ OVERALL TEST SUMMARY
TESTED: 2
PASSED: 2
FAILED: 0
IGNORED: 0
❌ BUILD FAILURE SUMMARY
• [SANITIZE] Sanitizers found 1 problem in 1 test. See the reports in build/artifacts/sanitize/sanitize: sanitize.log.
A clean build adds nothing to the test summary, even when JSON or SARIF reports are written.
On macOS, a leak sanitizer or :detect_leaks logs this warning at startup:
Backtraces
With :use_backtrace
set to :simple, each crashed test executable reruns one test case at a time.
The reruns run with the sanitizers’ options too, so each crashed test case is
named by its own finding.
:gdb backtraces fall back to :simple in instrumented builds, with a notice.
The leak sanitizer cannot run under a debugger, and a sanitizer’s own report
already includes the stack.
Artifacts
Each test with findings has a log of its sanitizer reports:
The configured reports are written beside the logs:
build/artifacts/sanitize/sanitize/sanitize.log
build/artifacts/sanitize/sanitize/sanitize.json
build/artifacts/sanitize/sanitize/sanitize.sarif
With :test_builds enabled, an ordinary test: build writes its logs and
reports to build/artifacts/sanitize/test/.
The artifacts are included in ceedling clean targets.
Interpreting results
Each sanitizer report begins with what it found and where, followed by the stack of calls that led there. Common findings include:
| Sanitizer | Finding | Description |
|---|---|---|
| ASan | heap-buffer-overflow |
Access past the end of a heap allocation |
| ASan | stack-buffer-overflow |
Access past the end of a local array |
| ASan | heap-use-after-free |
Access to memory already freed |
| ASan | double-free |
Memory freed twice |
| UBSan | runtime error: … |
Undefined behavior, such as signed overflow, an out-of-range shift, or a null pointer load |
| LSan | detected memory leaks |
Memory allocated and never freed |
A finding’s location is the first stack frame in your code, past the sanitizer’s own runtime.
Other plugins
A test executable built with sanitizers cannot run under Valgrind. Valgrind
builds are never instrumented by this plugin. A valgrind: build whose own
flags carry -fsanitize, such as flags it uses from :flags ↳ :test, stops
with a message naming them.
Coverage builds from the Gcov and Bullseye plugins are not instrumented either. Sanitizers and coverage instrumentation each alter the code they build, and are kept apart in their own builds.