Yocto
Why SBOM scanners miss what is in Yocto builds
Scanners read an output, such as a filesystem, a binary or a manifest, and infer what went in. A Yocto image hides most of that: static libraries and Rust crates are compiled into binaries, recipe patches change code without changing version numbers, the kernel configuration decides which code exists at all, and the build host's tools never reach the image. The alternative is to record the build, and keep Yocto's own patch and CVE metadata.
Updated
In one minute
- Build host: tools on the build machine shape the image and never appear in it. A scanner of the image cannot see them.
- Fetch: BitBake downloads sources, Rust crates, Go modules and tools. A scanner sees only what survives into the image.
- Recipes and patches: a recipe keeps the upstream version and applies security patches on top. A scanner matching versions reports CVEs that are already fixed.
- Compiled code: static libraries, crates and the kernel configuration end up inside binaries. A scanner sees one file where dozens of components went in.
- Image: production images often drop the package database. A scanner then has no list to read and reports little or nothing.
How a Yocto build differs from what scanners expect
A Yocto build is not a package manager install. Most of what ends up on the device is decided at build time, inside BitBake, in five layers. Scanners look at one layer after the fact and have to guess the rest.
1. The build host
BitBake runs with tools from the build machine. The HOSTTOOLS variable lists the tools on the build host
that build tasks may call, and the build system symlinks them into ${TMPDIR}/hosttools. BitBake also
builds its own tools through -native and -cross recipes. Host tools influence the image, and
by design they are not part of it.
2. Fetch
Recipes name their sources in SRC_URI, and the fetcher downloads them into DL_DIR. It looks
in DL_DIR first, then in PREMIRRORS, then upstream, then in MIRRORS. Rust crates
come through the crate:// fetcher, listed in a -crates.inc file that
cargo-update-recipe-crates generates from Cargo.lock. Go modules come through the
go-mod class or are vendored with go-vendor. BB_NO_NETWORK and
BB_ALLOWED_NETWORKS control what the fetcher may reach.
3. Recipes and patches
Each recipe pins an upstream version in PV and applies patches on top. A patch with a CVE:
line in its header marks that CVE as patched. The version number stays the upstream one, so the recipe can be fixed
while its version says otherwise.
4. Compiled code
Static libraries (the -staticdev packages), Rust crates and Go modules end up inside the binaries that use
them. The kernel and its modules are built from a product-specific configuration, so two platforms on the same kernel
version contain different code.
5. The image
The root filesystem. The package-management image feature installs package management tools and
preserves the package manager database. Without it, the package manager's data files are deleted from the root
filesystem, which is common on production images.
The shared state (sstate) cache
BitBake can restore the output of a task from the shared state cache instead of running it. Yocto's overview manual puts it plainly: if the prebuilt objects are available, the compiler is not needed, "thus, the compiler is not even downloaded". Tasks restored from sstate skip fetch, compile and install, which matters for any tool that relies on watching those steps. More on that in the section on caches below.
Why image and filesystem scanners miss most of it
-
No package database. A production image built without
package-managementgives a scanner no list to read, so it falls back to file heuristics or reports almost nothing. The result looks complete and is nearly empty. -
Static and vendored code. A static library or a crate compiled into
/usr/bin/fooleaves no package record.librsvgis an example: the current oe-core recipe lists more than 350 Rust crates, and on the device it is one shared library. -
Build host and fetched tools. They are never in the image, so never in an image scan, although a
compromised host tool or GitHub Action can compromise the image. The xz utils backdoor (CVE-2024-3094) went into the
library while it was being built, from release tarballs rather than the Git repository. In March 2025, CISA reported
that every version of the GitHub Action
tj-actions/changed-fileswas compromised for three days. -
Applied patches. The image says
opensslat an upstream version. The recipe applied the fix for a CVE affecting that version and tagged the patch withCVE:. A version-range match flags it anyway. This is the Yocto form of the distribution backport problem, and one of the main sources of the noise covered in how to reduce vulnerability noise.
The sum is a mixture of missing components and wrong findings, and nothing in the result tells you which is which. The same argument applies to containers in Docker image SBOMs and to language projects in open source SBOM generators.
Why binary scanning of firmware has to guess
Binary analysis matches strings, symbols and code signatures in extracted firmware against known components, then infers versions. Where the binary carries clear evidence, that works. In a Yocto image it often has to guess:
- stripped binaries with no version strings;
- statically linked code, where library boundaries are gone;
- compiled Rust, where crate boundaries disappear into one executable;
- backported patches, which are invisible in a version number;
- kernel code that the configuration compiled in or out.
Binary analysis is the right tool for firmware you did not build: supplier blobs, bought-in modules, a device you are evaluating. If you do not have the build, analyzing the binary is the only option. The decision rule is simple: if you build it, record it; if you only receive it, analyze it and ask the supplier for an SBOM from their build.
What Yocto's own SBOM and CVE tooling do well
The Yocto Project's SBOM work is good engineering, and it knows things no outside tool can. Its documentation says the SBOM is "a description of all the components you used, their licenses, their dependencies, their sources, the changes that were applied to them and the known vulnerabilities that were fixed".
create-spdx by release
- 4.3 (Nanbield): SPDX generation is on by default.
- 5.1 (Styhead): SPDX 3.0 by default instead of 2.2, and a
vexclass that gives external CVE tools the information they need. - 5.3 (Whinlatter):
SPDX_INCLUDE_COMPILED_SOURCES, which includes only the sources used to compile. - 6.0 (Wrynose, long-term support): SPDX 2.2 removed;
SPDX_INCLUDE_KERNEL_CONFIGandSPDX_INCLUDE_PACKAGECONFIGadded; recipes built with the Cargo, Go, PyPI, npm and CPAN classes get an ecosystem package URL.
The SBOM covers what BitBake builds, including the native tools it builds for the host. It is thorough about everything that has a recipe.
CVE status as VEX inside the SBOM
In SPDX 3, the build system writes each recipe's CVE status as VEX relationships: a patched CVE becomes "fixed",
linked to the patch files; an unpatched one "affected"; an ignored one "not affected", with the reason as the impact
statement. SPDX_INCLUDE_VEX sets how much goes in: none, current (the default,
vulnerabilities not already fixed in the upstream source) or all.
CVE_STATUS and the patch tag
CVE_STATUS records a decision per CVE, as a reason and an optional description. The reasons map to
patched (backported-patch, cpe-stable-backport, fixed-version and others),
unpatched (vulnerable-investigating) and ignored (cpe-incorrect, disputed,
not-applicable-config, not-applicable-platform, upstream-wontfix).
CVE_STATUS_GROUPS applies one decision to a group of CVEs, and CVE_PRODUCT sets the product
name used for matching.
# In a recipe or .bbappend
CVE_STATUS[CVE-2020-15523] = "not-applicable-platform: Issue only applies on Windows"
# In the header of a patch file
CVE: CVE-2022-3341
Upstream-Status: Backport [link to the upstream commit]
From cve-check to sbom-cve-check
cve-check matched recipe names (CVE_PRODUCT) and versions against vulnerability data. The
5.2 release notes warn that since the beginning of 2024 the NVD maintainers stopped annotating many CVEs with the
affected CPEs, so "the current CVE reports may look good but the reality is that some vulnerabilities are just not
reported". 5.2 also made the FKIE feed the default instead of NVD. Yocto 6.0 removed cve-check and
replaced it with the sbom-cve-check class, which runs Bootlin's
sbom-cve-check over
the SPDX output. On 5.0 to 5.3 you still have cve-check; treat it as legacy.
Where Yocto's SBOM stops
What Yocto cannot see is everything outside BitBake's metadata. We built core-image-sato twice on a
native ARM64 runner with 32 vCPUs, once with Yocto's SBOM generation and once without it, and compared Yocto's SPDX
3.0.1 SBOM, written as 1,632 files, with the CycloneDX SBOM CRACI recorded:
-
Components. CRACI's recorded SBOM had every component Yocto's SBOM had, plus 88 it missed: 23 host
packages on the build machine (such as Ubuntu's
cpio), 10 GitHub sources including the GitHub Actionactions/upload-artifact, and 55 other sources and tools fetched during the build. -
Rust crates. Both listed the same 653 Rust crate versions. In the release we built, Yocto's SBOM
listed them only as
.cratefiles with a download location, without apkg:cargopackage URL, so they could not be matched against advisories. Since Yocto 6.0, recipes built with the Cargo class carry a package URL for the recipe itself, such aspkg:cargo/librsvg; in the current code the individual crate downloads still have none. Check what your release writes. -
Cost. The build with Yocto's SBOM generation took 155 min 40 s and wrote 1,632 SPDX files. Without
it, the build took 88 min 48 s, 43% less.
SPDX_INCLUDE_SOURCES,SPDX_INCLUDE_COMPILED_SOURCES,SPDX_INCLUDE_KERNEL_CONFIGandSPDX_INCLUDE_PACKAGECONFIGwere on, so a default configuration costs less. Yocto's own documentation warns that with compiled sources included, the uncompressed SBOM ofcore-image-minimal"can grow from hundreds of megabytes to several gigabytes".
Native recipes such as cmake, llvm, ninja and qemu are in Yocto's
SBOM. The method, both SBOMs and the full report are on Yocto builds on CRACI.
Linux kernel CVEs: the per-configuration problem
The Linux kernel project announced on 13 February 2024 that it had become a CVE Numbering Authority, and it assigns CVEs to fixes generously. Its CVE documentation says the applicability of any specific CVE "is up to the user of Linux to determine", because "we do not know your use case, and we do not know what portions of the kernel that you use".
So a kernel version match produces hundreds of findings, most of them for code your configuration never compiles. Four hardware platforms with four kernel configurations give four different answers from the same kernel version. There are four ways to deal with it:
- Version matching only. Every CVE for the version is reported. Maximum noise.
-
Curated exclusion lists.
CVE_STATUSentries generated from the kernel's CVE data. In May 2026 oe-core removed its generated list forlinux-yocto, becausesbom-cve-checkdoes this itself from the CVE list the kernel feeds, with an unchanged CVE report and an SPDX almost 20 MB smaller. -
Filtering by what was compiled. With
SPDX_INCLUDE_COMPILED_SOURCESon for the kernel recipe,improve_kernel_cve_report.pychecks whether the files a CVE affects were used to build the kernel. Yocto's security manual says it "reduces CVE false positives by 70%-80%". - Recording the configuration.
SPDX_INCLUDE_KERNEL_CONFIG(6.0) writes the kernel configuration into the SBOM, so the answer can be checked later.
Worked example. One kernel CVE in a network driver. Platform A builds the driver; platform B does not.
| Platform | Driver compiled? | Version match | Compiled-sources filter |
|---|---|---|---|
| A | Yes | Reported | Reported: the affected files were built |
| B | No | Reported | Not reported: the affected files were not built |
For per-platform kernel CVE reports today, run Yocto's compiled-sources tooling once per machine configuration. Configuration is what decides the answer, so the report has to come from each configuration's build.
Caches, mirrors and vendoring: where any recorder must be careful
-
A build that restores everything from sstate or from a populated
DL_DIRfetches little or nothing. A recorder that sees fetches has to carry its evidence with those caches, or the record shrinks. - A recipe that points at a pre-vendored tarball shows one download, not the crates or modules inside it.
- Whatever you use, build release images from a known cache state, keep mirrors you control, and compare component counts between builds.
On CRACI, packages restored from CI caches stay in the SBOM, and each job's completeness state flags what the recording could not see.
What to run: a decision table
| Situation | What to use | What you get | What you still lack |
|---|---|---|---|
| You build the image in GitHub Actions | Record the build (CRACI); keep create-spdx if you rely on its patch and CVE status |
Host packages, fetched sources and Actions, a pkg:cargo URL per crate, ongoing monitoring |
Per-configuration kernel filtering: use Yocto's tooling |
| You build with Yocto in another CI system or on a build server | create-spdx with sbom-cve-check (or cve-check on older releases), kernel filtering with compiled sources |
Recipes, patches, CVE status as VEX | An inventory of host packages and fetched tools, kept separately |
| You receive firmware you do not build | Binary analysis, and a build SBOM from the supplier | Components inferred from the binary | Certainty about versions, patches and build inputs |
| You need kernel CVEs per platform | Yocto's compiled-sources tooling, per machine configuration | CVEs for the code each configuration builds | Non-kernel components, from your SBOM |
| You only have an image scanner | Use it for installed packages, if the package database is kept | Part of the runtime package list | Everything compiled in, patched or left on the build host; a clean result is not evidence |
What each method sees
| Component class | Image scanner | Binary analysis | Yocto create-spdx |
Build recording (CRACI) |
|---|---|---|---|---|
| Installed packages | Only if the package database is kept | Inferred from files | Yes | Yes, from the sources the build fetched |
| Static libraries | No | Inferred from signatures | Yes, as recipes | Yes, from the sources the build fetched |
| Rust crates | No | Inferred; crate boundaries disappear | As .crate downloads, without a per-crate package URL |
Yes, each with a pkg:cargo package URL |
| Go modules | No | Inferred from the binary | As module downloads of the recipe | Yes, when fetched during the build |
| Applied patches and CVE status | No | No | Yes, as VEX | No |
| Kernel configuration | No | No | Yes, with SPDX_INCLUDE_KERNEL_CONFIG (6.0) |
No |
| Host tools on the build machine | No | No | No (native recipes, yes) | Yes |
| Build tools fetched outside recipes, and GitHub Actions | No | No | No | Yes |
How CRACI fits
For Yocto builds that run in GitHub Actions, CRACI is the runner, with up to 32 vCPUs and 96 GB of RAM. It records what the build pulled in, including host packages, GitHub sources and Actions and other fetched sources, with a package URL for every Rust crate: in the published comparison, every component Yocto's SBOM had, plus 88 it missed. It matches the components against advisories, keeps re-evaluating the SBOM as new ones are published, and shows which builds contained a vulnerable dependency.
BitBake can already restrict its fetcher with BB_ALLOWED_NETWORKS. CRACI's
egress policy adds a second control at the runner, checked before the job
starts and failing closed, so a script or tool that goes around the fetcher still cannot reach a host you did not
allow.
For supply chain coverage, CRACI's SBOM can replace create-spdx. Yocto's SPDX still carries the applied
patches and the CVEs marked as fixed, which CRACI does not record, so keep create-spdx on if you rely on
those. The CRACI docs include an example that builds core-image-sato. Why recording beats scanning in
general is on build-time SBOMs; Yocto tools are compared on
embedded security tools compared and
CRACI vs Timesys Vigiles.
When CRACI is not the answer
- You rely on patch-level CVE status, such as
CVE_STATUSdecisions and patched or ignored CVEs as VEX. That lives in Yocto's SPDX: keep it. - You need per-configuration kernel CVE reports today. Use Yocto's compiled-sources tooling.
- You do not build the firmware. Use binary analysis.
- Your Yocto builds run outside GitHub Actions, or must run on your own infrastructure. CRACI offers managed runners on European infrastructure today; other CI systems and customer-hosted runners are on the roadmap.
- You need the Yocto job SBOM in SPDX specifically. CRACI's SBOM in the published comparison was CycloneDX; Yocto's own SBOM is SPDX.
Building connected devices for the EU market? Read what the CRA requires, how VEX works, and how CRACI fits telecom and network equipment, industrial IoT and consumer electronics.
Yocto CVE checks and SBOMs: frequently asked questions
Why can't my SCA tool scan our Yocto-based products?
Because most SCA tools read manifests, lockfiles or an image's package database, and a Yocto build decides its contents inside BitBake. Production images often drop the package database, static libraries and Rust crates are compiled into binaries, recipe patches leave upstream version numbers in place, and the build host's tools never reach the image.
Can I scan a Yocto image or firmware with Trivy, Grype or a binary scanner?
You can, and you will get a partial answer. An image scanner reads the installed packages if the package database is kept, and misses what is compiled in. A binary scanner infers components from strings and signatures and has to guess with stripped binaries, static code and compiled Rust. Neither sees the build host or the patches a recipe applied.
Does Yocto's SBOM include Rust crates and Go modules?
It lists them as the sources of the recipe that uses them. In the release we built, each Rust crate appeared as a .crate download with a download location and no pkg:cargo package URL. Since Yocto 6.0, recipes built with the Cargo and Go classes get an ecosystem package URL for the recipe itself, not for each crate or module download.
Why does my scanner report CVEs that our Yocto recipes already patch?
Because it matches version numbers. A Yocto recipe keeps the upstream version and applies security fixes as patches, tagged with a CVE: line or recorded in CVE_STATUS. A version-range match cannot see the patch. Yocto's own SPDX records those CVEs as fixed, linked to the patch files.
What replaced cve-check in Yocto?
The sbom-cve-check class, in Yocto 6.0. It runs Bootlin's sbom-cve-check over the SPDX output of the build. Releases 5.0 to 5.3 still have cve-check, which matched recipe names and versions; the 5.2 release notes warn that its reports can miss vulnerabilities because many CVEs no longer carry CPE data.
How do I get Linux kernel CVEs for my own kernel configuration?
Turn on SPDX_INCLUDE_COMPILED_SOURCES for the kernel recipe and run improve_kernel_cve_report.py, which checks whether the files each CVE affects were compiled. Yocto's security manual says it reduces CVE false positives by 70% to 80%. Run it per machine configuration, since each configuration builds different code.
Does the shared state cache affect the SBOM?
It can. Tasks restored from sstate skip fetch and compile, so a tool that relies on watching those steps sees less. Yocto's create-spdx works from recipe metadata. For any method, build release images from a known cache state and compare component counts between builds.
Is Yocto's built-in SBOM enough for the CRA?
The CRA asks for an SBOM covering at least the top-level dependencies, and Yocto's SBOM is thorough about what BitBake builds. It does not list host packages on the build machine, GitHub Actions or tools fetched outside recipes. Whether that is enough is your call; the SBOM is evidence for your process, not compliance by itself.
Bring a Yocto build to the demo
Book a demo and we will run one of your BitBake builds on a CRACI runner and walk through what it fetched.
Book a demo