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
apt-cache policy splint
splint -version 2>/dev/null || trueOn 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 policyis read-only and shows installed/candidate repository versions.A distribution may patch packaging or compatibility without creating a new upstream feature release.
|| truekeeps 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
sudo apt update
sudo apt install splintAPT 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 updaterefreshes package indexes; it does not upgrade every installed package.apt installrequires 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
#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
malloccan return null, but the code dereferences the result without a check.When
sizeis zero, evenbuffer[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
cc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -c unsafe_buffer.cCompiler 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=c17selects a known C language mode.-Wall -Wextra -Wpedanticenables a broad portable warning baseline, not literally every diagnostic.-Wconversionand-Wshadowfind additional risky conversions and name hiding but may need project-specific triage.-ccompiles 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
splint -strict unsafe_buffer.cExpect 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
-strictenables 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
#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.
callocinitializes 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
cc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -fanalyzer -c safe_buffer.c
splint -strict safe_buffer.cReview 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
-fanalyzerperforms 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
#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
onlydescribes an exclusive ownership reference that must be transferred or released correctly.nullpermits a null result, forcing callers to model allocation failure.notnullsays 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
gcc -std=c17 -Wall -Wextra -Wpedantic -fanalyzer -o app src/*.cGCC 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
-fanalyzerinto 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
scan-build --status-bugs --keep-going -o analyzer-reports make -j2scan-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-buildinterposes on compiler invocations so configuration must use the analyzer-aware compiler setup.--status-bugsmakes found reports visible to CI through the exit status.--keep-goingasks supported builds to continue and can expose more findings in one run.-oretains 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
cc -std=c17 -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined -o tests tests.c src/*.c
ASAN_OPTIONS=halt_on_error=1 ./testsThe 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
Compile every change with strict reviewed warnings and treat new warnings as failures.
Run fast unit tests and selected sanitizer jobs on every merge request.
Run GCC or Clang path-sensitive analysis using the real build database/configuration.
Keep Splint only for annotated legacy components where it adds signal.
Run deeper sanitizers, fuzzers, dependency/SBOM scans, and cross-translation-unit analysis on scheduled or release pipelines.
Require human security review for risky boundaries and verify fixes with regression tests.
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
Splint project site provides the historical release, manual, annotations, and papers; its latest posted release is 3.1.2 from 2007.
GCC Static Analyzer options documents
-fanalyzer, current diagnostics, cost, and limitations.Clang Static Analyzer documents the maintained path-sensitive C/C++/Objective-C analyzer.
Clang analyzer command-line usage compares scan-build and CodeChecker and explains real-build integration.
SEI CERT C Coding Standard provides review guidance for secure C rules and recommendations.
Comments and corrections