This is a C++ static analyzer to enforce the lifetime safety profile:
- C++ Core Guidelines Lifetime Safety Profile: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#SS-lifetime
- Spec: https://github.com/isocpp/CppCoreGuidelines/blob/master/docs/Lifetime.pdf
The code is based on Matthias Gehre's fork of LLVM.
Please make sure conan2 and cmake is avaiable.
# Configuration
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLOCAL_CLANG=$(brew --prefix llvm@17)# Build
cmake --build build -j
# Install (optional)
sudo cmake --install build# Install llvm via conancd conan
conan export --name=llvm --version=17.0.2 .cd -
# Configuration
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
# Build
cmake --build build -j
# Install (optional)
sudo cmake --install build
# Or you can try cppship https://github.com/qqiangwu/cppship
cppship build -r
sudo cppship installcd integration_test
bash run_tests.shYou can see integration_test/example.cpp for what the lifetime profile can check.
Assume cppsafe is already installed.
cppsafe example.cpp -- -std=c++20Pass cppsafe arguments before --, pass compiler arguments after --.
Note that cppsafe will detect system includes via c++, you can override it via the environment variable CXX.
CXX=/opt/homebrew/opt/llvm/bin/clang cppsafe example.cpp -- -std=c++20Note: cppship should be used with std17 or above, since cpp17 has changed the rule for temporaries.
Generally, you should use cppsafe with compile_commands.json.
Generate compile_commands.json via cmake, assume it's in build. And run:
cppsafe -p build a.cpp b.cpp c.cppcppsafe will define __CPPSAFE__ when compiling your code.
Warnings can be suppressed by function/block/statement/declaration.
[[gsl::suppress("lifetime")]]voidfoo1(int* p)
{
*p;
}
voidfoo2(int* p)
{
[[gsl::suppress("lifetime")]]
{
*p;
*p;
}
[[gsl::suppress("lifetime")]]int x = *p;
[[gsl::suppress("lifetime")]]
*p;
}Cppsafe will check nullness for pointers. The rules are:
voidf1(int* p); // pset(p) = {null, *p}voidf2(vector<int>::iterator p); // pset(p) = {*p}using A [[clang::annotate("gsl::lifetime_nonnull")]]= int*;
using B = int*;
voidf3(A a, B b);
// pset(a) = {*a}// pset(b) = {null, *b}// For pointer classes, if is has `operator bool`, then null will be inferedvoidf4(std::function<void()> f, span<int> s);
// pset(f) = {null, *f}// pset(s) = {*s}By default, cppsafe aims to provide lowest false-positive, lots of checks are not performed. You can use the following options to disable or enable more checks.
To minimize false positives, cppsafe will not check use after move by default. You can use --Wlifetime-move to enable it.
To minimize false positives, cppsafe will not check nullness of pointers by default. But if you systematically use gsl::not_null, try it via --Wlifetime-null/--Wno-lifetime-call-null
// -Wlifetime-nullvoidfoo(int* a, gsl::not_null<int*> b, int* c)
{
// pre: pset(a) = {null, *a}// pre: pset(b) = {*b}// pre: pset(c) = {null, *c}int x = *a; // warning: dereference a possibly null pointerint y = *b; // okassert(c != nullptr);
int z = *c; // ok
}You can use --Wno-lifetime-call-null to disable all nullness check for parameters and return values when --Wlifetime-null is on.
voidfoo(int* a, gsl::not_null<int*> b, int* c)
{
// pre: pset(a) = {*a}// pre: pset(b) = {*b}// pre: pset(c) = {*c}int x = *a; // okint y = *b; // okint z = *c; // ok
}If --Wlifetime-null is not enabled:
voidfoo(bool x)
{
int* p = nullptr;
if (x) p = newint{};
if (x) *p; // warningif (x) {
assert(p);
*p; // ok
}
if (p) {
*p; // ok
}
}Cppsafe currently cannot handle this. Maybe in the future, cppsafe will use path-sensitive check to do null check.
Add member function postcondition check.
Member function contracts are not infered well, which may need lots of manual annotation to reduce warnings.
struct[[gsl::Pointer(int)]] Ptr
{
double* m;
// infered post: pset(return) = {global}double* get()
{
return m; // infer: pset(m) = {**this}
}
// warnings are generated only if --Wlifetime-post
};Note: member function contract inference is likely to change to reduce the number of manual annotations.
Requires global variables of Pointer type to always point to variables with static lifetime.
// post: pset(return) = {null, global}
Test* Get();
voidtest()
{
auto* p = Get();
int d;
p->p = &d;
// warning: global pointer points to non-static variables// triggered only if --Wlifetime-global
}Get warnings when the flow-sensitive analysis is disabled on a function due to forbidden constructs.
voidfoo(int* p)
{
++p; // silently disable lifetime checks, use --Wlifetime-disabled to trigger a warning
*p = 0;
}Enforce output parameter validity check in all paths.
intgenerate(bool cond, int** out)
{
assert(out);
// post: pset(out) = {null, *p}// post: pset(*out) = {invalid}if (cond) {
// *out is {invalid} in this branch, warnings are triggered only if --Wlifetime-outputreturnERROR;
}
*out = newint{10};
returnOK;
}template <classT>
void__lifetime_pset(T&&) {}
voidfoo(int* p)
{
__lifetime_pset(p);
}template <classT>
void__lifetime_contracts(T&&) {}
voidFoo();
structDummy {
intFoo(int*);
voidBar(int*);
};
__lifetime_contracts(&Foo);
// __lifetime_contracts will derive the actual function from the call// note a return value is required to make semantics check pass__lifetime_contracts(Dummy{}.Foo(nullptr));
__lifetime_contracts([]{
Dummy d;
d.Bar(nullptr); // <- will be inspected
});The following annotations are supported now.
[[clang::annotate("gsl::lifetime_const")]]- put it after parameter name or before function signature to make it
lifetime_const
struct[[gsl::Owner(int)]] Dummy
{
int* Get();
[[clang::annotate("gsl::lifetime_const")]]voidFoo();
};
voidFoo(Dummy& p [[clang::annotate("gsl::lifetime_const")]]);
voidFoo([[clang::annotate("gsl::lifetime_const")]] Dummy& p);[[clang::annotate("gsl::lifetime_in")]]- mark a pointer to pointer or ref to pointer parameter as an input parameter
// by default, p is viewed as an output parametervoidFoo(int** p);
voidFoo2(int*& p);
voidBar([[clang::annotate("gsl::lifetime_in")]]int** p);
voidBar2([[clang::annotate("gsl::lifetime_in")]]int*& p);We need to use clang::annotate to mimic the effects of [[gsl::pre]] and [[gsl::post]] in the paper.
structTest
{
[[clang::annotate("gsl::lifetime_pre", "*z", ":global")]][[clang::annotate("gsl::lifetime_post", "return", "x", "y", "*z", "*this")]][[clang::annotate("gsl::lifetime_post", "*z", "x")]][[clang::annotate("gsl::lifetime_post", "m", ":invalid")]]int* Foo(int* x [[clang::annotate("gsl::lifetime_pre", ":global")]], int* y, int** z, int* m);
};Special symbols null, global, invalid must be prefixed with : to avoid name conflicts with parameters.
Annotate parameters acting as both input and output.
// by default, Point* and Pointer& are output parametersvoidf1(int** a)
{
// pre: pset(a) = {null, *a}// pre: pset(*a) = {invalid}// post: pset(*a) = {null, global}
}
voidf2([[clang::annotate("gsl::lifetime_in")]]int** a)
{
// pre: pset(a) = {null, *a}// pre: pset(*a) = {null, **a}
}
voidf3([[clang::annotate("gsl::lifetime_inout")]]int** a)
{
// pre: pset(a) = {null, *a}// pre: pset(*a) = {null, **a}// post: pset(*a) = {null, **a, global}
}This annotates a type alias of a raw pointer as nonnull.
using A [[clang::annotate("gsl::lifetime_nonnull")]]= int*;
typedefint* B [[clang::annotate("gsl::lifetime_nonnull")]];Used in pointer methods.
struct[[gsl::Pointer]] Ptr
{
[[clang::annotate("gsl::lifetime_capture", "p")]]voidfoo(int* p)
};
voidfoo()
{
Ptr p;
int a;
int b;
p.foo(&a);
p.foo(&b);
// pset(p) == {global, a, b}
}This is to reset a moved-from object.
classValue
{
public:[[clang::reinitializes]]voidReset();
};There are some defects to the underlying model, currently I have no idea how to cope with it.
To minimize false-positives, the following code will not be flagged.
voidfoo(std::vector<std::unique_ptr<int>>& cont, std::unique_ptr<int>& p)
{
for (auto& x : cont) {
p = std::move(x);
}
for (auto& x: cont) {
use(x);
}
}
voidbar(std::vector<std::unique_ptr<int>>& cont, std::unique_ptr<int>& p)
{
for (size_t i = 0; i < cont.size(); ++i) {
p = std::move(cont[i]);
}
for (auto& x: cont) {
use(x);
}
}boolfoo(int** out)
{
if (cond) {
// pset(*out) = {invalid}, we allow it, since it's so commonreturnfalse;
}
*out = newint{};
returntrue;
}You can use --Wlifetime-output to enable the check, which enforces that *out must be initialized on all paths.
A Pointer* parameter will be inferred as an output variable, and we will add two implicit preconditions:
- If Pointer is a raw pointer or has default contructor
pset(p) = {*p}pset(*p) = {Invalid}
- Otherwise
pset(p) = {*p}pset(*p) = {**p}