Review & Verify Updated 2026-09 9 min read View as Markdown

Dependency hygiene in C, where there is no package manager

No registry means no slopsquatting — and also no lockfile, no audit command, and no automatic way to learn that the library you vendored in 2022 has a CVE.

C is the odd one out in this series. There is no npm, no PyPI, no module proxy — so the slopsquatting attack that dominates the other languages does not exist here. What replaces it is quieter and, in aggregate, probably worse: you have vendored code with no manifest, no version, and no mechanism that will ever tell you it is vulnerable.

The C-specific hallucination: APIs, not packages#

A model will not invent libfoo for you to pip install. It will invent a function in a library you really are using.

c
/* All plausible. None of these exist. */
size_t n = strlcpy_s(dst, src, sizeof dst);
json_t *v = json_object_get_default(obj, "key", fallback);
int rc = curl_easy_setopt_str(h, CURLOPT_URL, url);

The failure mode is benign in one sense — it will not link — but it wastes a real amount of time, because the code reads correctly and the error surfaces at link time with an undefined-symbol message that points at the call site rather than the misunderstanding.

Two flavours worth knowing:

  • Wrong platform. strlcpy and strlcat are BSD; glibc did not have them for many years. Generated code uses them freely and then fails on Linux.
  • Wrong version. A function added in a later release of a library you pin an older version of. The signature is right, the symbol is absent.

Catch it with: a compile, which is free and immediate — and by pinning your documentation. If your agent has a docs MCP server pointed at the version you actually build against, this mostly stops happening.

Vendoring, done deliberately#

Most C projects vendor. The mistake is vendoring without a record, which is what turns a dependency into an unowned liability.

third_party/
  cJSON/
    VERSION            <- v1.7.18
    ORIGIN             <- https://github.com/DaveGamble/cJSON
    SHA256             <- of the release tarball you actually used
    PATCHES/           <- every local change, as a patch file
    LICENSE
    cJSON.c cJSON.h

Four small files, and they answer the questions you will actually be asked: what version is this, where did it come from, have we changed it, and can we prove the tarball was not tampered with.

Patches as files rather than edits in place is the discipline that matters most. A vendored library with untracked local modifications cannot be updated — someone eventually has to diff two thousand lines to find out what you changed, so nobody updates it, and it sits there accumulating CVEs for five years.

Track CVEs yourself, because nothing else will#

There is no npm audit. You need a manifest and something to check it against.

Generate an SBOM. CycloneDX and SPDX are the standard formats and most build systems can emit one; failing that, a hand-maintained list of name, version, origin, sha256 is enormously better than nothing.

shell
# CMake can emit one; or generate from your vendoring records
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build
# then feed the manifest to a scanner
grype sbom:./sbom.json
trivy sbom ./sbom.json

Subscribe to what you depend on. For the handful of libraries that matter — TLS, compression, image and font parsing, anything touching untrusted bytes — subscribe to the security announce list. That is a five-minute task that will one day save you a bad week.

Set a review cadence. Quarterly, look at the list and ask whether each entry is still maintained, still needed, and still current. In C, dependencies do not get updated by a bot; they get updated because someone looked.

Prefer the standard library, then the platform#

Every dependency is code you now maintain the security of. C's standard library is small, but the platform underneath it is not:

Generated code reaches forOften unnecessary because
a library for string buildingsnprintf with a length, or a small arena
a dynamic-array library40 lines of realloc with geometric growth
a hash-map libraryfine — but pick one and use it everywhere
a JSON library for one config fileconsider a simpler format, or one file of parsing
a logging frameworkfprintf(stderr, …) plus a level check
a threading abstractionC11 <threads.h> or pthreads directly

The rule worth putting in your AGENTS.md: do not add dependencies; if you think we need one, say so and stop. Most languages cannot afford that rule. C can, and the reduced attack surface is worth the extra code.

Build reproducibility#

If two people building the same commit get different binaries, you cannot reason about what is deployed.

  • Pin the toolchain, not just the libraries. A container image or a Nix expression, with the compiler version fixed.
  • -fdebug-prefix-map so absolute build paths do not end up in the binary.
  • Set SOURCE_DATE_EPOCH to remove timestamps.
  • Record what you linked. ldd output, or better, static linking so the answer is unambiguous.

Static versus dynamic linking is also a security decision, and it cuts both ways. Dynamic means a distro security update fixes you without a rebuild. Static means you must rebuild and redeploy for every CVE — but you know exactly what is in the binary. Static plus a working rebuild pipeline is the better position; static without one is the worst of both.

Hardening applies to dependencies too#

Vendored code is compiled by you, so your flags protect it:

makefile
HARDEN := -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection \
          -fPIE -Wl,-z,relro,-z,now,-z,noexecstack

That is free mitigation across every line of third-party code in your build, including the parts nobody has read. Combined with sanitizers in your debug build, it is the highest-value dependency control available in C — because it does not depend on knowing which dependency is vulnerable.

The whole policy, in five lines

  1. Vendor with VERSION, ORIGIN, SHA256 and PATCHES/. Never edit in place.
  2. Keep an SBOM and scan it on a schedule.
  3. Subscribe to announce lists for anything parsing untrusted input.
  4. Default answer to a new dependency is no.
  5. Hardening flags on everything, including third-party code.

Common questions#

Is a C package manager worth adopting?#

Conan and vcpkg both work and both solve the manifest problem properly. They are worth it for a project with more than a handful of dependencies or one that builds on several platforms. For a project with three vendored libraries, disciplined vendoring is less machinery for the same result.

How do I know if a vendored library is still maintained?#

Last release date, open issue count and whether security reports get responses. If the answer is "no release in four years and unanswered CVE reports", that is a decision to make deliberately — fork and own it, or replace it — rather than discovering it during an incident.

Should I vendor or use the system package?#

System packages get security updates for free, which is a real advantage. Vendoring gives you a reproducible build and no surprises when a distro bumps a version. For anything long-lived, vendor with the discipline above; for a tool that only ever runs on one controlled platform, the system package is less work.

Does an SBOM matter for a small project?#

It matters the first time someone asks "are you affected by CVE-2026-xxxx", and it takes minutes to answer with one and hours without. If you ship software to anyone else, that question is coming.

Get the C agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for C. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.