Splint is a time capsule with sharp edges. It can look at a tiny C program and describe ownership loss, possible null dereference, undefined reads, and bad bounds with surprising clarity. It can also stumble over modern headers and language features because its latest upstream release, 3.1.2, dates to July 2007. The honest way to use it today is as one specialist voice in a larger review—not the security verdict.

What static analysis can and cannot prove

  • Static analysis examines source/compiled representations without executing every real runtime scenario.

  • A tool can find plausible defect paths, type/contract violations, ownership mistakes, and suspicious data flow.

  • Most practical analyzers are neither sound nor complete: false positives and false negatives exist.

  • A clean report does not prove memory safety, secure design, correct authorization, race freedom, or absence of vulnerabilities.

  • Results depend on compile flags, headers, macros, platform models, annotations, and whether the analyzed build matches production.

Splint’s useful niche

  • Legacy C projects already carrying Splint annotations and suppression policy.

  • Teaching explicit ownership, nullability, initialization, and buffer contracts.

  • A secondary lint signal for C dialects and headers it can parse reliably.

  • Reviewing small isolated components when modern toolchains are unavailable.

  • It is not a good sole gate for modern C++, contemporary language extensions, complex generated code, or an unannotated large system.

Choose the first tool by project reality

  • Existing annotated Splint baseline: keep it while adding a maintained analyzer.

  • Current GCC C build: enable reviewed warnings and evaluate -fanalyzer.

  • Clang C/C++ build: use Clang Static Analyzer or clang-analyzer checks through maintained tooling.

  • Large collaborative findings workflow: evaluate CodeChecker and compilation-database integration.

  • Memory-sensitive executable tests: add supported sanitizers and fuzzing regardless of the static analyzer.

1. Check availability before changing the system

Ubuntu/Debian shellbash
apt-cache policy splint
splint -version 2>/dev/null || true
On Ubuntu 24.04, the repository candidate is a distro build of Splint 3.1.2. If it is not installed, the version command produces no normal version line.

Package availability is not active maintenance

  • apt-cache policy is read-only and shows installed/candidate repository versions.

  • A distribution may patch packaging or compatibility without creating a new upstream feature release.

  • || true keeps this diagnostic sequence going when the executable is absent; do not use that pattern to hide CI analysis failures.

  • Record the exact package origin/version in reproducible build documentation.

  • Other distributions may omit Splint or package it differently.

2. Install only when the project still needs Splint

Ubuntu 24.04 development machinebash
sudo apt update
sudo apt install splint
APT refreshes repository metadata and requests confirmation before installing Splint and its packaged data files.

Risk level: caution. Review the command before running it.

Installation changes system packages

  • apt update refreshes package indexes; it does not upgrade every installed package.

  • apt install requires administrative authorization and changes the host, so prefer a pinned container/tool image in CI.

  • Review repository origin, candidate version, downloads, and disk changes before confirmation.

  • Do not run package installation inside an application production container at startup.

  • If Splint cannot parse the project, remove it from the new-project plan rather than weakening the source to satisfy an obsolete parser.

3. Start with a deliberately unsafe C program

unsafe_buffer.cc
#include <stddef.h>
#include <stdlib.h>
 
int first_byte(size_t size)
{
    unsigned char *buffer = malloc(size);
    int result = buffer[0];
 
    buffer[size] = 0;
    buffer = NULL;
    return result;
}

Four defects hide in nine lines

  • malloc can return null, but the code dereferences the result without a check.

  • When size is zero, even buffer[0] is outside the allocated object.

  • Allocated indexes run from zero through size - 1; buffer[size] is always one past the allocation.

  • Assigning null to the only pointer loses the allocation and creates a memory leak.

  • The function also reads uninitialized allocated storage, so its returned value is indeterminate.

4. Let the compiler speak first

Directory containing unsafe_buffer.cbash
cc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -c unsafe_buffer.c
Compiler warnings vary by implementation and version. A normal warning pass may not report every path-sensitive allocation, bounds, or ownership defect in this function.

Warnings are the inexpensive baseline

  • -std=c17 selects a known C language mode.

  • -Wall -Wextra -Wpedantic enables a broad portable warning baseline, not literally every diagnostic.

  • -Wconversion and -Wshadow find additional risky conversions and name hiding but may need project-specific triage.

  • -c compiles without linking and still performs front-end diagnostics.

  • Do not assume silence means safety; ordinary warnings are not a full interprocedural analyzer.

5. Run Splint in strict mode

Directory containing unsafe_buffer.cbash
splint -strict unsafe_buffer.c
Expect diagnostics around possibly null storage, use-before-definition, bounds, and unreleased fresh storage. Exact wording/location can differ with the packaged build and system headers.

Read each warning as a proposed contract violation

  • -strict enables an intentionally noisy collection of checks.

  • A possible-null diagnostic asks whether every path proves allocation success before dereference.

  • A use-definition diagnostic tracks whether bytes receive values before becoming rvalues.

  • Bounds diagnostics distinguish readable/writable ranges and can reveal the classic one-past-end mistake.

  • Fresh-storage warnings model ownership: the last owning reference disappeared without free.

6. Fix the program instead of suppressing the symptoms

safe_buffer.cc
#include <stddef.h>
#include <stdlib.h>
 
int zeroed_first_byte(size_t size, int *value)
{
    unsigned char *buffer;
 
    if (value == NULL || size == 0) {
        return -1;
    }
 
    buffer = calloc(size, sizeof(*buffer));
    if (buffer == NULL) {
        return -1;
    }
 
    *value = buffer[0];
    free(buffer);
    return 0;
}

The revised ownership path closes cleanly

  • The caller supplies an output pointer and receives an explicit success/failure status.

  • The function rejects null output and zero-size requests before allocation.

  • calloc initializes all allocated bytes to zero, so reading the first byte is defined.

  • sizeof(*buffer) follows the pointed-to type and avoids duplicating a type name.

  • Allocation failure is checked before dereference.

  • Every successful allocation reaches exactly one free, and no pointer is used afterward.

7. Compile and analyze the corrected code

Directory containing safe_buffer.cbash
cc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -fanalyzer -c safe_buffer.c
splint -strict safe_buffer.c
Review both tools independently. Splint may report compatibility/style findings even when the current compiler analyzer accepts the memory paths.

Different engines reveal different assumptions

  • GCC -fanalyzer performs path-sensitive interprocedural bug finding and is more expensive than ordinary compilation.

  • GCC documents its analyzer as neither sound nor complete and, in the current release documentation, suitable for C.

  • Splint’s annotation model can express contracts that ordinary source does not expose.

  • A disagreement is a review prompt—not permission to keep only the quieter tool.

  • Pin compiler/tool versions in CI because diagnostics evolve.

Splint annotations add machine-readable intent

owned_buffer.h (Splint-annotated legacy API)c
#include <stddef.h>
 
/* The caller receives ownership and must free a non-null result. */
/*@only@*/ /*@null@*/ unsigned char *
buffer_create(size_t size);
 
/* The function borrows data; it does not retain or release it. */
size_t buffer_checksum(/*@notnull@*/ const unsigned char *data, size_t size);

Annotations are part of the API contract

  • only describes an exclusive ownership reference that must be transferred or released correctly.

  • null permits a null result, forcing callers to model allocation failure.

  • notnull says the parameter must point to an object when called.

  • C compilers see these as comments; Splint interprets them.

  • Incorrect annotations can hide real defects or create noise, so review them like executable interface specifications.

  • Modern projects may prefer compiler attributes, standardized annotations where available, or analyzer-specific contracts supported by actively maintained tools.

Suppression is technical debt with an owner

  • First determine whether the report is a real defect, a missing contract, an analyzer limitation, or dead code.

  • Prefer a local, narrow annotation or suppression with a reason and issue reference.

  • Do not disable entire classes such as null or bounds checking just to reach zero output.

  • Set an expiry/review trigger when tool versions or surrounding code change.

  • Track baseline findings separately and fail CI on newly introduced reviewed-severity defects.

Current alternative: GCC static analyzer

C project rootbash
gcc -std=c17 -Wall -Wextra -Wpedantic -fanalyzer -o app src/*.c
GCC compiles the program while exploring selected interprocedural paths for issues such as leaks, null dereferences, double frees, use-after-free, descriptor misuse, tainted indexes, and out-of-bounds access.

Use production compile definitions

  • The shell glob selects matching C files but does not reproduce complex project include paths, generated sources, macros, or link libraries.

  • Integrate -fanalyzer into the real build system so analyzed flags match production.

  • Analyzer time/memory can be substantial; schedule full runs appropriately and keep normal warnings on every build.

  • Review the exact GCC version’s documented checks because the analyzer evolves.

  • Never concatenate untrusted filenames/options into shell build commands.

Current alternative: Clang Static Analyzer

Configured C/C++ projectbash
scan-build --status-bugs --keep-going -o analyzer-reports make -j2
scan-build wraps the build, writes path reports under analyzer-reports, continues after supported build failures, and returns a failing status when analyzer bugs are found.

Analyze the build you actually ship

  • scan-build interposes on compiler invocations so configuration must use the analyzer-aware compiler setup.

  • --status-bugs makes found reports visible to CI through the exit status.

  • --keep-going asks supported builds to continue and can expose more findings in one run.

  • -o retains HTML/path reports outside a temporary directory; treat reports as potentially sensitive source/path data.

  • LLVM documentation recommends CodeChecker for richer collaborative storage, comparison, filtering, and cross-translation-unit workflows.

Runtime sanitizers catch executed defects

C project test buildbash
cc -std=c17 -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined -o tests tests.c src/*.c
ASAN_OPTIONS=halt_on_error=1 ./tests
The instrumented test binary stops and reports when an executed path triggers supported address or undefined-behavior checks.

Dynamic evidence complements static paths

  • AddressSanitizer detects classes of invalid memory access on executed paths.

  • UndefinedBehaviorSanitizer checks selected undefined operations.

  • -g, modest optimization, and frame pointers improve diagnostic stacks while retaining realistic code transformation.

  • Sanitizers impose overhead and do not cover unexecuted paths; feed them strong unit, integration, fuzz, and regression tests.

  • Do not combine incompatible sanitizers blindly, and run on supported toolchain/platform targets.

Security review goes beyond memory warnings

  • Threat-model trust boundaries, assets, attackers, abuse cases, privileges, and recovery.

  • Review integer ranges, parsing, protocol states, format strings, command/path construction, authentication, authorization, cryptography, randomness, secrets, logging, and error paths.

  • Audit third-party code, generated code, compiler/linker hardening, build provenance, and vulnerability response.

  • Use fuzzing for parsers/state machines and race-focused tools/tests for concurrency.

  • Map findings to a standard such as CWE or CERT C where useful, but fix root causes rather than chasing labels.

A practical CI ladder

  1. Compile every change with strict reviewed warnings and treat new warnings as failures.

  2. Run fast unit tests and selected sanitizer jobs on every merge request.

  3. Run GCC or Clang path-sensitive analysis using the real build database/configuration.

  4. Keep Splint only for annotated legacy components where it adds signal.

  5. Run deeper sanitizers, fuzzers, dependency/SBOM scans, and cross-translation-unit analysis on scheduled or release pipelines.

  6. Require human security review for risky boundaries and verify fixes with regression tests.

  7. Archive tool versions, commands, findings, suppressions, and dispositions for reproducibility.

Common Splint failures decoded

  • Parse errors in system headers: Splint’s old parser/model does not understand the active headers/extensions; use compatible stubs/flags only when maintained, otherwise switch tools.

  • Hundreds of library warnings: analysis environment or annotations are missing; do not globally suppress before isolating project code.

  • Possible null pointer: prove the check on every path or correct the API contract.

  • Fresh storage not released: ownership is lost, transferred without annotation, or cleaned only on some paths.

  • Used before definition: initialize the object/field or establish an explicit contract that the tool can verify.

  • Bounds warning: confirm element count versus byte count and remember the last valid index is length minus one.

  • Tool passes but sanitizer fails: executed behavior exposed a path/model the static tool missed.

  • Tool warns after a correct fix: investigate analyzer limitations, then document the narrow suppression with evidence.

Completion checklist

  • Splint’s 2007 upstream status and project-specific reason for retaining it are documented.

  • Exact source, headers, macros, target, compiler, and analyzer versions match a reproducible build.

  • Compiler warnings, a maintained static analyzer, runtime sanitizers, tests/fuzzing, and human review cover complementary risks.

  • Every high-confidence finding has an owner, severity, root-cause fix, and regression test.

  • Suppressions are narrow, justified, reviewed, and expire/revalidate.

  • No report is marketed as proof of security; release risk and residual blind spots are explicit.

Primary references