← All posts
Engineering

Why bomwerk never shells out to git

The Problem

A software bill of materials, or SBOM, is the inventory of everything that actually ships inside a product: every library, every version, every dependency. Building an accurate one for a C/C++ project means answering a very ordinary question: which exact commit is each vendored library pinned to?

That sounds like something a computer should be able to tell you in a fraction of a second. It can. The complication isn't finding the answer, it's finding it safely when the code under inspection came from someone else, which is the entire point of a scanning tool.

The Obvious Solution

Git already knows the answer, so the natural move is to ask it directly:

$ git submodule status
 4c1f8ae0e6b5d7f2a91c3b8e7d4f6a2c9b1e5d80 vendor/zlib (v1.3.1)
 a91c3b8e7d4f6a2c9b1e5d804c1f8ae0e6b5d7f2 vendor/mbedtls (heads/main)

Split each line on whitespace, keep the first column, done. Fifteen minutes of work including a test. It runs the git program as a subprocess, meaning your scanner hands off the work to another already-running program instead of doing it itself, and reads back whatever that program prints. It works perfectly on your own repositories, on your own laptop, which is exactly the setting where nothing is wrong yet.

Why the Obvious Solution Doesn't Work at All

Here's what changes the moment that repository didn't come from you: when you run git inside a directory, git doesn't just report facts. It reads that directory's own settings file, .git/config, and follows instructions written inside it. Not your settings. Whatever settings the folder came with.

Git's config format documents several settings that name an external program for git to run automatically during ordinary operations, things like core.fsmonitor, core.pager, and diff.external. A .git/config file can legitimately contain a line that runs a script the moment you type an everyday git command, and that script runs with your permissions, not the repository author's.

This isn't theoretical. It's been the exact mechanism behind real, patched vulnerabilities:

  • CVE-2018-11235 let a malicious submodule name contain a directory-traversal sequence, so cloning a booby-trapped repository could run arbitrary code on the machine that cloned it.
  • CVE-2022-24765 showed that on a shared machine, git could be tricked into reading and trusting configuration from a directory it had no business trusting, simply because it happened to look like a git repository.

Different exact mechanisms, same root cause: git, by default, extends a surprising amount of trust to the directory it's currently sitting in. A scanning tool's whole job is inspecting code it did not write and has no reason to trust, so inheriting that trust automatically is exactly backwards.

A fair objection: cloning from a URL never hands you a hostile .git/config, so who's actually at risk? Anyone who hands over a folder instead of a link. A vendor's source drop. A customer's tarball you unzip to scan. A CI cache restored from somewhere you didn't build. bomwerk exists specifically to scan exactly those, so treating any of them as automatically trustworthy isn't an option.

There's a duller, less dramatic reason too: shelling out to git means git has to be installed, at a compatible version, with output stable enough to parse reliably. Plenty of CI build agents don't have git installed at all. bomwerk runs on machines we don't get to choose.

What bomwerk Does Instead

Git's on-disk layout is a documented, stable file format, so instead of running the git program, bomwerk reads those files directly, the same way you'd open a text file.

To find the commit a submodule is pinned to: read <submodule>/.git, which is a folder in a normal checkout or a one-line pointer file inside a submodule, follow it to git's internal directory, then read the HEAD file inside that directory. If HEAD holds a commit id directly (git calls this a detached HEAD, meaning it's pointing straight at a commit instead of a named branch), that's the answer. If HEAD instead points at a named reference, such as refs/heads/main, bomwerk reads that small file too, and falls back to a compact index file called packed-refs if the reference has been archived there.

No subprocess ever runs. .git/config is never opened. Git doesn't even need to be installed on the machine.

Four rules keep this honest:

  • Every read has a size cap. A crafted repository can't make bomwerk read an unbounded amount of data just by placing an oversized file where a small one is expected.
  • Nothing resolves outside the repository being scanned. If a pointer file tries to lead somewhere outside the folder under scan, bomwerk refuses to follow it and reports that submodule as not checked out instead. This directly closes the path-traversal shape behind CVE-2018-11235.
  • No .. path segments, ever, in a submodule path or an internal reference. Git itself never produces .. in its own internal references, so rejecting one can only ever catch a tampered file, never a legitimate one.
  • Never crash, never guess. A submodule that's declared but not actually checked out still shows up in the output, marked low confidence with a warning, instead of either crashing the scan or silently vanishing.

What It Actually Cost Us

We rebuilt a slice of git's own internal logic by hand, and hand-rebuilding a mature tool's internals is exactly how bugs that tool fixed years ago get reintroduced. We keep a growing library of real and deliberately hostile test repositories specifically because of that risk. We also don't yet support every internal storage format git can use; a newer layout called "reftable" isn't handled today. And this whole approach only works because git's file layout happens to be documented and stable. If it weren't readable as plain files, we'd have to skip that case rather than fall back to running git anyway.

That's the trade, stated plainly. A scanner that only reads files is something you can audit line by line. A scanner that executes another program is a dependency you didn't choose to take on, sitting inside the very tool you bought to find dependencies you didn't choose to take on.


If your current SBOM tool runs on C/C++ repositories today, it's worth asking whether it shells out to git, and what it does with a submodule that's declared but never checked out. Curious how this connects to the EU Cyber Resilience Act reporting clock? An accurate inventory like this is what makes "are we affected?" answerable in minutes. Or skip ahead: we'll scan one repository free and show you what your current tool reports versus what's actually pinned in your tree.

Scanning a C/C++ repo and not sure what your SBOM is missing? We'll scan one repo free and show you the diff against whatever you run today.

Get a free scan of one repo