104% more reported CVEs per day in 2026 than last year.

Yocto

Yocto SBOM and builds on CRACI

Yocto can write a detailed SPDX SBOM for every image. Built side by side, CRACI's recorded SBOM had every component Yocto's did, 88 it missed, and a package URL for each of the 653 Rust crates Yocto lists without one, in 43% less build time.

Updated

Built side by side: core-image-sato

Components listed: 10% more in CRACI's SBOM

CRACI SBOM
953
Yocto SBOM
865

88 components Yocto's misses, such as host packages and GitHub Actions; none only in Yocto's

Rust crates with a package URL, so they can be checked for vulnerabilities

CRACI SBOM
653
Yocto SBOM
0

Both list the same 653; Yocto's only by file name and download URL

Build time and cost per build, 32 vCPU ARM64

CRACI
1 h 29 min $5.68
CRACI + Yocto SBOM
2 h 36 min $9.96
GitHub + Yocto SBOM*
5 h 11 min $15.60
43% lower cost: CRACI vs CRACI + Yocto SBOM
64% lower cost: CRACI vs GitHub + Yocto SBOM*

*Estimate for a GitHub-hosted 32-core ARM64 runner at $0.050 a minute, with CRACI about twice as fast.

Get the full comparison ↓

How to generate an SBOM with Yocto

The OpenEmbedded build system writes SPDX SBOMs through the create-spdx class. Whether it is on, and which SPDX version it writes, depends on the Yocto release:

  • Kirkstone (4.0): off by default. Add INHERIT += "create-spdx". SPDX 2.2.
  • Nanbield (4.3): SBOM generation is on by default from this release.
  • Scarthgap (5.0): on by default, SPDX 2.2. SPDX 3.0 is available through the create-spdx-3.0 class.
  • Styhead (5.1) and later: on by default, SPDX 3.0.

You set it in a configuration file such as conf/local.conf. On a current release there is usually nothing to add; to turn it off, use INHERIT_DISTRO:remove = "create-spdx".

# conf/local.conf

# Kirkstone (4.0): SBOM generation is off by default, so turn it on
INHERIT += "create-spdx"

# Scarthgap (5.0): switch the output from SPDX 2.2 to SPDX 3.0
INHERIT_DISTRO:remove = "create-spdx"
INHERIT_DISTRO:append = " create-spdx-3.0"

# Any release: indented JSON you can read in an editor
SPDX_PRETTY = "1"

Other SPDX_ variables add detail. SPDX_INCLUDE_SOURCES describes the source files behind each host tool and target package, which makes the output much larger. Recent releases add options for the kernel configuration, PACKAGECONFIG features, supplier information and package URLs for Cargo, Go, PyPI, npm and CPAN sources.

Where the SBOM files land

Build an image and the top-level SBOM appears next to it, as IMAGE-MACHINE.spdx.json in tmp/deploy/images/MACHINE/ inside the build directory. Related files are in the same directory and in tmp/deploy/spdx. On Kirkstone the per-recipe documents come as an IMAGE-MACHINE.spdx.tar.zst archive.

bitbake core-image-minimal

# The image SBOM
ls tmp/deploy/images/qemux86-64/core-image-minimal-qemux86-64.spdx.json

# Per-recipe and package documents
ls tmp/deploy/spdx/

# Current releases: an SBOM for one recipe, from its metadata only
bitbake busybox -c create_recipe_sbom

CycloneDX with meta-cyclonedx

Yocto itself writes SPDX only. For CycloneDX, the meta-cyclonedx layer generates an SBOM from the image's root filesystem, plus a separate VEX file with the patched and ignored CVEs the build system knows about. Check out the layer's branch for your release, add it to your layers and inherit its class:

# In your sources directory, on the branch for your release
git clone https://github.com/iris-GmbH/meta-cyclonedx.git
cd meta-cyclonedx
git checkout scarthgap

# conf/bblayers.conf
BBLAYERS += "${BSPDIR}/sources/meta-cyclonedx"

# conf/local.conf
INHERIT += "cyclonedx-export"
# Optional: include build-time packages, not only what is in the image
CYCLONEDX_RUNTIME_PACKAGES_ONLY = "0"

The files land next to the image in DEPLOY_DIR_IMAGE, with the suffixes .cyclonedx.bom.json and .cyclonedx.vex.json. The default is CycloneDX 1.6, set with CYCLONEDX_SPEC_VERSION. By default only runtime packages are included, which keeps the SBOM small but can leave out components that reach the image without being installed as packages.

What the Yocto SBOM leaves out

Yocto's SBOM is thorough about what BitBake builds. In SPDX 3 it covers the image, every recipe including the build-time native ones, their sources, the patches applied and the vulnerabilities marked as fixed. For core-image-sato that came to 1,632 separate files. To see what it leaves out, we built the image on a CRACI runner, native ARM64 with 32 vCPUs, and compared Yocto's SBOM with the one CRACI recorded, component by component, after normalizing names across all of those files.

Of 953 components in CRACI's SBOM, 865 were also in Yocto's. None was only in Yocto's. The differences are in what Yocto cannot see, and in how it identifies what it does list.

Rust crates without a package URL

Both SBOMs contain the same 653 versions of 559 Rust crates, such as cairo-rs and zune-jpeg, which librsvg compiles in. Yocto lists each one only as a .crate file and a crates.io download URL, inside the recipe that uses it. Not one carries a pkg:cargo package URL. CRACI records every crate with one.

That decides whether you can monitor them for vulnerabilities. Vulnerability matching works on package URLs: an advisory for a crate names it as pkg:cargo/..., and a file name inside another recipe's source list gives SBOM and vulnerability tools nothing to match it against. With Yocto's SBOM alone, a vulnerable crate in your image goes unreported.

With CRACI's, every crate is matched against advisories, each package in the build shows its vulnerability status, and CRACI keeps re-evaluating the SBOM as new advisories are published, long after the build finished.

What only CRACI found

The 88 components missing from Yocto's SBOM came from outside BitBake's metadata:

  • 23 host packages installed on the build machine, such as Ubuntu's cpio 2.15, libc6, locales and python3-jinja2. BitBake runs with them, and Yocto's SBOM does not list them.
  • 10 GitHub sources, including patchelf, btrfs-progs and the GitHub Action actions/upload-artifact, pinned to the commit the workflow ran
  • Other sources and tools fetched during the build, such as pseudo-prebuilt, tzdata and Python build tools like hatchling and setuptools_scm

None of these is installed in the image, but all of them are in the critical path of the build: a compromised host tool or Action can compromise your image without ever being part of it. CRACI lists them because it sees the traffic coming into the build.

What the built-in SBOM costs

Yocto's SBOM generation is not free. On the same 32 vCPU runner, the build with it took 155 min 40 s and wrote the SBOM as 1,632 separate SPDX files. The build without it, with CRACI recording, took 88 min 48 s. At CRACI's rate of $0.002 per vCPU-minute, that is about $9.96 against $5.68 per build, before the time your team spends waiting for it.

The gap is wider against GitHub-hosted runners. CRACI runs builds about twice as fast, thanks to faster hardware, shorter queue times and caching, so the same build with Yocto's SBOM on a GitHub-hosted 32-core ARM64 runner would take about 5 h 11 min. At $0.050 a minute that is $15.60. The CRACI build costs $5.68: 64% less, or about 2.7 times cheaper. GitHub list prices as of September 2026; included minutes do not cover larger runners.

Yocto's SPDX output does carry things CRACI does not record, such as the patches a recipe applies and the CVEs it marks as fixed. If your process depends on those, keep create-spdx on and use CRACI's SBOM alongside it. If not, CRACI's SBOM covers every component Yocto's did, and more, on its own.

Why this matters for supply chain security

What goes into a build decides what comes out of it, whether or not it is installed in the image. Attackers know this, and the build is where recent supply chain attacks have gone in:

  • Attacks hide in the build. The xz utils backdoor (CVE-2024-3094) was in versions 5.6.0 and 5.6.1. Its trigger sat in the release tarballs rather than the Git repository, and the malicious code went into the library while it was being built. Reading the source did not reveal it; knowing exactly which tarball the build fetched does.
  • CI dependencies are targets too. In March 2025, CISA reported that every version of the GitHub Action tj-actions/changed-files was compromised for three days. An Action runs inside your build job, which is why CRACI lists the ones your workflow used, pinned to the commit that ran.
  • Rust crates disappear into binaries. Cargo compiles crates into the programs that use them, so a vulnerable crate ships inside the image without ever appearing as a package. If the SBOM lists it only as a file name, vulnerability tools cannot match it either. Scanning the finished binary afterwards has to guess what went in; recording the build does not.
  • You cannot respond to what you cannot see. When an advisory lands for a crate or a build tool, you need to know which builds used it. CRACI keeps every build's SBOM and shows which builds contained the vulnerable dependency, direct or transitive, so you know the period you were affected. That only works when every component has a package URL to match on.
  • A build should only reach what it needs. CRACI's egress policy blocks connections to hosts the build does not need, so a compromised dependency that tries to send your secrets to an attacker's server is stopped, even in an attack nobody has seen before.

Customers and regulations such as the Cyber Resilience Act also ask manufacturers of connected devices to know what goes into their products. An SBOM that covers the whole build is evidence that holds up when they ask.

Get the full comparison

The component lists behind these numbers, both SBOMs, and how we built and compared them.

  • The 88 components only CRACI found, and the 865 both SBOMs listed
  • How each SBOM identifies the 653 Rust crate versions
  • Both SBOM files: CRACI's CycloneDX and Yocto's SPDX
  • The build setup, runner size and timings

Why Yocto builds are hard to run in CI

A full image build compiles a toolchain, a kernel and hundreds of packages from source. On the standard 2-core runners most CI services offer, that can take hours, so many embedded teams keep a dedicated build server under a desk or in a rack, and that server becomes one more machine to patch, back up and explain to an auditor.

A Yocto build also downloads a great deal: source tarballs, Git repositories and patches from upstream projects and mirrors. Those downloads are what your SBOM has to account for, and what regulations such as the Cyber Resilience Act ask manufacturers of connected devices to know about and keep track of.

What CRACI gives a Yocto build

Runners sized for BitBake

CRACI runners scale from 1 to 32 compute units, where each unit is 1 vCPU and 3 GB of RAM. A Yocto build can ask for the largest size, 32 vCPUs and 96 GB of RAM, from the workflow itself. Jobs run in isolated virtual machines and the runs still appear in GitHub like any other Actions job.

Minutes are metered per second and billed per vCPU-minute, so a large runner costs more per minute and finishes sooner. See pricing for current rates.

Network access you decide

BitBake can already restrict its fetcher to a list of hosts with BB_ALLOWED_NETWORKS. CRACI adds a second control at the runner: an egress policy that is checked before the job starts and fails closed, so a recipe, a script or a compromised tool that goes around the fetcher still cannot reach a host you did not allow. CRACI alerts you on policy violations.

A record of what the build fetched

CRACI observes the traffic a job sends to package and source hosts and records it as build evidence. For a Yocto build that means an independent account of the sources that actually came over the network, next to the SBOM that Yocto writes itself.

A minimal workflow

The Yocto example in the CRACI docs builds core-image-sato for qemux86-64. The parts that matter for CRACI are the runner size and a fetcher configuration that uses the official Yocto source mirror:

jobs:
  build:
    runs-on: craci
    steps:
      - uses: cracicorp/setup@v1
        with:
          size: "32"
      # install the host packages from the Yocto quick start, then:
      - run: |
          echo 'SOURCE_MIRROR_URL = "https://downloads.yoctoproject.org/mirror/sources/"' >> conf/local.conf
          echo 'BB_ALLOWED_NETWORKS = "downloads.yoctoproject.org"' >> conf/local.conf
          bitbake core-image-sato

The docs guide has the full workflow, including host dependencies and locale setup, and the runner configuration reference lists every setup option.

Things to know

  • Builds run on CRACI's managed runners on European infrastructure, from GitHub Actions. Customer-hosted runners and other CI systems are on the roadmap.
  • Point BitBake at mirrors CRACI can see, such as the official Yocto source mirror, rather than arbitrary hosts. The docs guide shows a working setup.
  • Keeping the downloads directory and shared state cache between runs is what makes repeat builds fast.
  • Native ARM64 runners are available at the same rate as x86-64, with limited capacity.

Building connected devices for the EU market? Read what the CRA requires, and how CRACI works for industrial IoT and consumer electronics. Comparing Yocto SBOM tools? See CRACI vs Timesys Vigiles.

Yocto SBOMs and CRACI: frequently asked questions

Does Yocto generate an SBOM?

Yes. The OpenEmbedded build system generates an SPDX SBOM by default since Yocto 4.3 (Nanbield), through the create-spdx class. For an image it writes IMAGE-MACHINE.spdx.json to tmp/deploy/images/MACHINE/, covering components, licenses, dependencies, sources, applied patches and fixed vulnerabilities. On Kirkstone (4.0), add INHERIT += "create-spdx" to turn it on.

Which SPDX version does Yocto produce?

SPDX 3.0 by default since Yocto 5.1 (Styhead), which replaced 2.2 as the default. Scarthgap (5.0) produces SPDX 2.2 by default and can switch to 3.0 with the create-spdx-3.0 class.

How do I get a CycloneDX SBOM from Yocto?

Add the meta-cyclonedx layer on the branch for your release and inherit cyclonedx-export in local.conf. It writes a .cyclonedx.bom.json SBOM and a .cyclonedx.vex.json file next to the image, in CycloneDX 1.6 by default.

What does the Yocto SBOM miss?

In a side-by-side build of core-image-sato, CRACI's recorded SBOM had all 865 components in Yocto's SBOM plus 88 more: host packages on the build machine such as Ubuntu's cpio, GitHub sources and Actions, and other sources fetched during the build. Yocto also lists its 653 Rust crate versions only by file name and download URL, without a package URL, which vulnerability matching relies on.

Can I monitor vulnerabilities in a Yocto build?

Yes, with CRACI. Vulnerability matching works on package URLs, and Yocto's built-in SBOM lists Rust crates only by file name and download URL, so they cannot be matched. CRACI records the Rust crates and the build machine's packages with the package URLs advisories are published against (pkg:cargo, pkg:deb), including all 653 crate versions in core-image-sato. It shows each package's vulnerability status and keeps re-evaluating the SBOM as new advisories are published.

Can CRACI replace the Yocto SBOM?

For supply chain coverage, yes. In the same comparison, CRACI's SBOM had every component in Yocto's built-in SBOM, 88 more and a package URL for every Rust crate, and the build without Yocto's SBOM generation took 43% less time. Yocto's SPDX output still carries recipe metadata that CRACI does not record, such as the patches applied and the CVEs a recipe marks as fixed. If you rely on those, keep create-spdx on.

Can a CRACI runner handle a full Yocto image build?

CRACI runners go up to 32 vCPUs and 96 GB of RAM, set from the workflow. The CRACI docs include a Yocto example that builds core-image-sato on the largest size.

Which CI does a Yocto build on CRACI use?

GitHub Actions. Your workflow runs on CRACI's managed runners and the runs appear in GitHub like any other Actions job. Other CI systems and customer-hosted runners are on the roadmap.

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