A static analysis tool for C.
Ward accepts C99 source files, annotated with permissions, and verifies that permissions are correct for all top-level functions. A permission is a constraint on the context in which some code is allowed to run, and can be used to verify things like locking, signal-safety, program phases (e.g., initialization and shutdown), and memory allocation (e.g., disallowing allocation in performance- or safety-critical code).
C programs tend to have a fair amount of global state, which can produce subtle errors if used incorrectly. Ward is designed to make it easier to use this sort of state correctly. It’s a fairly liberal analysis—the goal is to call attention to suspicious code, not verify everything precisely.
Ward is written in Haskell using the language-c
package for parsing C99. You need Stack to build it.
> git clone [email protected]:evincarofautumn/Ward.git
> cd Ward
> stack setup # if this is your first time using Stack
> stack build
> stack exec ward -- --help
Ward - A static analysis tool for C.
Usage: ward CPP [-M|--mode html|compiler|graph] [-C|--config FILE] [-q|--quiet]
PATH... [-P|--preprocess FLAG]
Verify Ward permissions for all top-level functions.
Available options:
CPP Name of preprocessor.
-M,--mode html|compiler|graph
Output mode style (default 'compiler'). The 'graph'
mode does not run analyses, just parses the source
files and emits inferred call graph.
-C,--config FILE Read permission information from configuration FILE.
-q,--quiet Suppress output except for errors.
PATH... Paths to C source files.
-P,--preprocess FLAG Pass FLAG to preprocessor.
-h,--help Show this help text
stack install
will place a copy of the executable in ~/.local/bin
(Mac & Linux) or %APPDATA%\local\bin
(Windows).
Ward accepts a path to a C preprocessor (e.g., gcc
), a list of paths to C sources, one or more config files, and additional flags to pass to the preprocessor. On OS X, you’ll want to pass -fno-blocks
or -U__BLOCKS__
because the C parsing library used by Ward does not yet support Apple’s block extension. A typical invocation looks like this:
ward gcc foo.c bar.c --config=my.config -- -I/some/include/path -U__BLOCKS__
Ward should build and run on OS X and Linux; it may work on Windows with a suitable preprocessor installed, but this hasn’t been tested.
Ward determines how to carry out its analysis based on config files with the following format. You can check out the grammar of config files for more details.
// This is a comment. It begins with two slashes and continues to the end of the line.
In order to use a permission, you must declare it in the config by specifying its name.
my_permission;
A permission may be annotated with a description string for documentation purposes and to help produce better error messages.
my_permission "permission to do the thing";
After the permission name and before the description (if present), you can specify modifiers for the permission. Currently the only modifier is implicit
:
alloc implicit "can allocate";
With the implicit
modifier, the alloc
permission will be implicitly granted to all functions unless they explicitly waive it (see “Annotating Your Code”).
Permissions may have any number of additional restrictions, which consist of an ASCII rightwards arrow ->
followed by a Boolean expression using the operators &
(and), |
(or), and !
(not). Restrictions specify that if the permission to the left of the arrow is used, then the context must have or lack some additional permissions specified on the right. Like permissions, restrictions can also have description strings.
lock_held "assume the lock is held";
take_lock "take the lock"
-> !lock_held "cannot take the lock recursively";
Ward will only enforce annotations for functions specified in the config, so you can add Ward annotations incrementally to an existing codebase. Enforcement directives begin with .enforce
and accept:
- A file name, to enforce annotations for all functions declared or defined in that file;
- A function name, to enforce annotations for a particular function; or
- Both a file name and function name, to enforce annotations for a single function if defined in the given file—this is useful for
static
functions.
.enforce "locks.h"; // All functions declared in a path ending in "locks.h"
.enforce foo_bar; // All functions named "foo_bar"
.enforce "locks-internal.c" do_stuff; // The function "do_stuff" in "locks-internal.c"
Ward accepts annotations in the form of function-level attributes, which specify permissions and the actions Ward should take with them.
__attribute__((ward(action(perm, perm, …), action(perm, perm, …), …)))
An action may be any of the following:
Action | Effect |
---|---|
need(p) |
When this function is called, p must be in the context. |
use(p) |
Same as need , but expresses the intent that p is used directly, and should be subject to restrictions specified in the config. |
deny(p) |
When this function is called, p must not be in the context. |
waive(p) |
If p was declared implicit , this function is not granted p . |
grant(p) |
After this function is called, p is added to the context. |
revoke(p) |
After this function is called, p is removed from the context. |
Here’s a typical example, where alloc
is declared implicit
:
#define PERM(...) __attribute__((ward(__VA_ARGS__)))
void must_not_allocate(void)
PERM(waive(alloc));
void take_lock(void)
PERM(need(lock), revoke(lock), grant(locked));
void must_have_lock_held(void)
PERM(need(locked));
void release_lock(void)
PERM(need(locked), revoke(locked), grant(lock));
These annotations enforce the following properties:
-
must_not_allocate
:waive(alloc)
: this function cannot call functions that implicitlyneed(alloc)
-
take_lock
:need(lock)
: if a function takes the lock, it must also be annotated withneed(lock)
revoke(lock)
: when the lock is held, it can’t be recursively lockedgrant(locked)
: a function annotated withneed(locked)
can only be called when the lock is held
-
must_have_lock_held
:need(locked)
: the lock must have been taken already before entering this function
-
release_lock
:need(locked)
: the lock must be held in order to release itrevoke(locked)
: when the lock is released, a function annotated withneed(locked)
can no longer be calledgrant(lock)
: the lock can be taken again after it is released
By implicitly granting alloc
, you only need to annotate those functions that are explicitly not allowed to allocate. You can do a similar thing with e.g. signal_unsafe implicit;
—functions are not required to be signal-safe by default, but you want to make sure that no signal-safe function calls a signal-unsafe function!
In this way, Ward is pay-as-you-go: you only need to annotate and verify the specific source files and permissions you’re interested in, and you can define whatever meanings you like for a permission, because it’s just a label.
On a project of nontrivial size (dozens of kloc), Ward consumes a large amount of memory and operates slowly due to retaining a large amount of language-c
AST information. The preferred method for checking a large project is to build a call map for each translation unit separately using --mode=graph
, and then load & check all the call maps together. You can see an example of this in check-mono.sh
.
Ward currently does not handle indirect calls. In practice this is not a significant limitation, as the vast majority of calls in typical C code are direct, but it may be addressed in the future.
It seems to be usable and effective in practice, but it’s not very well tested. We’re working on adding a comprehensive test suite.
It also doesn’t handle non-trivial control flow very well, so you may need to restructure some code in order to make it checkable. For example, Ward cannot verify this, because the locked
permission is conditional:
void foo(bool lock) {
if (lock) {
take_lock();
}
…
if (lock) {
release_lock();
}
}
Ward doesn’t know whether the if
branch will be taken, so it conservatively assumes that it requires the permission to lock. This may be addressed in the future to support more coding patterns. However, as a workaround, currently it can verify this alternative formulation:
void foo_locked(void) {
take_lock();
foo_unlocked();
release_lock();
}
void foo_unlocked(void) {
…
}
(And this is arguably better coding practice, anyway.)