CVEAssert¶
CVEAssert is an LLVM compiler pass plugin that instruments programs
by inserting runtime checks into functions identified by a CVE description.
It consumes a CVE description encoded in JSON, which is parsed into
an internal representation containing the affected source file, affected function,
CWE identifier, and remediation strategy. Based on this
description, CVEAssert selects and applies the appropriate sanitizer
to each affected function. CVEAssert can optionally be linked with
the libresolve runtime library to enforce stack and heap bounds protections. The pass is executed early in the compilation pipeline to allow LLVM's analysis and optimization framework to optimize the injected instrumentation.
Tip
For a step-by-step walkthrough of instrumenting a fix with CVEAssert, see the remediation example.
Architecture Diagram¶

Types of Sanitizers¶
| Type | Sanitizer |
|---|---|
| Arithmetic | Divide by Zero |
| Arithmetic | Integer Overflow |
| Memory | Heap OOB |
| Memory | Stack OOB |
| Memory | Free Nonheap |
| Memory | Null Pointer Deref |
| Other | Operation Masking |
Directory Structure¶
.
├── ArithmeticSanitizer.cpp - Arithmetic sanitizer
├── BoundsCheck.cpp - Heap/Stack bounds check
├── CVEAssert.cpp - Pass entry point
├── IRUtils.cpp - LLVM-generated helper functions
├── NullPointerSanitizer.cpp - Null pointer sanitizer
├── OperationMasking.cpp - Operation masking sanitizer
└── Vulnerability.hpp - Internal data structure to parse CVE description
Supported Sanitizers¶
| Sanitizer | Behavior |
|---|---|
| Divide by Zero | Instruments division and remainder operations with runtime checks that remediate when the divisor is zero. |
| Integer Overflow | Instruments arithmetic operations using nsw/nuw and inserts overflow checks where undefined behavior may occur. |
| Heap Out-of-Bounds | Instruments heap loads, stores, and getelementptr instructions with runtime checks to enforce heap bounds. |
| Stack Out-of-Bounds | Instruments stack alloca, load, store, and getelementptr instructions with runtime checks to enforce stack bounds. |
| Null Pointer Dereference | Instruments pointer load and store instructions with runtime checks that check for null dereference. |
| Operation Masking | Replaces selected 'undesirable' function calls with guarded calls that validate operands before execution. |
| Free Nonheap Memory | Instruments calls to free with runtime checks that ensure argument is a heap-allocated pointer. |
Note
The CVE description must include an undesirable-function field
for the Operation Masking sanitizer to be applied. If this field
is not present, Operation Masking is not enabled.
Supported Values¶
Here is a table of weakness identifiers and alternatives that can be used to activate specific sanitizers.
Common Mappings¶
| Weakness Identifiers | Sanitizer |
|---|---|
190 |
Integer Overflow |
369 |
Divide by Zero |
476 |
Null Pointer Dereference |
590 |
Free Nonheap |
121 |
Stack OOB |
122 |
Heap OOB |
Note
Stack and heap can be activated simultaneously with
these weakness identifiers 123,125,131, 787. See "Additional Mappings" below
for other supported ids.
Additional Mappings¶
CVEAssert recognizes a handful of other cwe-id values for compatibility purposes, see below mapping:
| Weakness Identifiers | Sanitizer |
|---|---|
0 |
Enables All Sanitizers |
123 |
Write What Where |
119 |
OOB |
787 |
OOB Write |
125 |
OOB Read |
131 |
Incorrect Buffer Size |
1335 |
Incorrect Bitwise Shift |
Remediation Strategies¶
Remediation strategies define how sanitizers respond to detected errors encountered at runtime.
| Remediation Strategy | Behavior |
|---|---|
| Continue | Invalid operations are ignored and return 0 |
| Exit | Inserts exit function call with specified exit code |
| None | Does not perform remediation |
| Recover | Transfer control to a recovery handler using longjmp |
| Saturate (Sat) | Use saturating arithmetic |
| Widen | Widen potentially overflowing intermediate operations |
| Wrap | Use 2's complement arithmetic |
Defaults¶
Arithmetic¶
The default remediation stategy
for arithmetic sanitizers is Wrap, both when
no strategy is specified and when
an invalid sanitizer-strategy combination
is encountered.
Note
If the divide by zero sanitizer is selected with the continue strategy, undefined results will return the dividend.
Memory¶
The default remediation strategy for
memory sanitizers is Continue, both when no
strategy is specified and when an invalid
sanitizer-strategy combination is encountered.
Note
Unlike the other strategies, RECOVER is semi-automatic.
This strategy requires the programmer to insert a
jmp_buf construct within the program and insert
additional logic to cause the program to call setjmp
to transfer control to a recovery handler.
Instrumentation Examples¶
Arithmetic¶
#include <limits.h>
int add_user_input(int a, int b) {
int sum = a + b;
return sum;
}
int main(int argc, char **argv) {
int x = INT_MAX;
int y = 1;
int result = add_user_input(x, y);
return result;
}
When CVEAssert instruments this code, it inserts a check that detects signed integer overflow before performing the addition. If an overflow is detected, the compiler-generated remediation logic invokes the configured remediation strategy. In this example, the saturate strategy is applied, causing the result to be clamped to INT_MAX instead of overflowing.
// Integer overflow
#include <limits.h>
int add_user_input(int a, int b) {
// -- Instrumentation inserted by CVEAssert --
int sum;
// Check for signed integer overflow before addition
if (__builtin_add_overflow(a, b, &sum)) {
// Overflow detected -> apply remediation
sum = __resolve_add_saturate_int(a, b);
}
// If no overflow, sum contains correct result.
return sum;
}
int main(int argc, char **argv) {
int x = INT_MAX;
int y = 1;
int result = add_user_input(x, y); // result == INT_MAX (saturated)
return result;
}
Memory¶
// Out-of-bounds Write
#include <stdlib.h>
void heap_oob_example() {
int *buf = (int *)malloc(4 * sizeof(int));
buf[0] = 10;
buf[1] = 20;
buf[2] = 30;
buf[3] = 40;
buf[4] = 50; // Heap out-of-bounds write
free(buf);
}
heap_oob_example.
The program allocates space for four integers on the heap. Valid indices range from zero to three. The write to buffer[4] accesses memory beyond the allocated object and results in a heap out-of-bounds write.
When CVEAssert instruments this code, a bounds check is inserted before the store operation. At runtime libresolve retrieves the bounds metadata associated with buffer, and the instrumented code verifies that the target address falls within the valid allocation range. Since buffer[4] lies outside of the object bounds of the allocated object, the bounds check fails and the configured remediation strategy is invoked before the invalid write is performed.
#include <stdlib.h>
// Libresolve runtime interface (simplified)
typedef struct { void *base, void *limit} ShadowObjBounds;
extern ShadowObjBounds __resolve_get_bounds(void *addr);
void heap_oob_example() {
// malloc replaced with __resolve_malloc hook
int *buf = (int *)__resolve_malloc(4 * sizeof(int));
buf[0] = 10;
buf[1] = 20;
buf[2] = 30;
buf[3] = 40;
// -- Instrumentation inserted by CVEAssert --
ShadowObjBounds bounds = __resolve_get_bounds(buf);
char *base_addr = (char*)&buf[4];
size_t access_size = sizeof(int);
if (base_addr + access_size - 1 >= bounds.limit) {
// compiler-generated remediation handler
__cve_remediation_handler();
}
buf[4] = 50; // Write occurs if check passes
__resolve_free(buf);
}
Operation Masking¶
// Operation Masking Example
#include <string.h>
void process_user(const char *input) {
char dest[8];
// Copy user into fixed-sized buffer
// Vulnerable: strcpy does not check
// length before writing contents to
// dest
strcpy(dest, input);
printf("Processed: %s\n", dest);
}
int main(int argc, void **argv) {
process_user(argv[1]);
return 0;
}
The original program allocates a fixed buffer on the stack and uses
the strcpy() function to copy the user input into the destination
buffer. Because strcpy() does not verify the size of the destination
buffer is large enough to hold the source string, an oversized input
can overflow the buffer and result in undefined behavior.
When CVEAssert instruments this code, it generates a sanitized
wrapper for strcpy() that returns the first argument rather than
performing the copy operation. The compiler then redirects calls to
strcpy() to the sanitized wrapper, preventing the unsafe operation
from being executed.
#include <string.h>
// Sanitized wrapper generated by CVEAssert
// Operation masking: return first argument instead of copying.
char *strcpy_masked(char *dest, const char *src) {
return dest;
}
void process_user(const char *input) {
char dest[8];
// Original call to strcpy is replaced by wrapper
strcpy_masked(dest, input);
printf("Processed: %s\n", dest);
}
int main(int argc, char **argv) {
process_user(argv[0]);
return 0;
}
Testing¶
To verify correct IR transformations and binary behavior, we developed a testing suite with regression testing. The suite contains testcases for each sanitizer and tests that the resulting binaries perform the intended behaviors with and without the remediation instrumentation. The testing suite can be found in resolve-cveassert/tests and the tests can be executed by calling make test.