Skip to main content
Version: 029-rc5

Pantavisor Manifest Audit

pv-manifest-audit.bbclass produces a deterministic listing of every entry in an image rootfs (path, type, mode, ownership, symlink targets, device nodes) and compares it against a reference checked into a layer. Drift is surfaced as a unified-diff patch — printed to the bitbake log, written to the deploy directory, and (in strict mode) used to fail the build.

This catches a class of regressions that ordinary CI does not:

  • a package starts shipping a setuid binary it never had before,
  • a recipe accidentally drops a previously-installed file,
  • ownership flips from root:root to messagebus:messagebus,
  • a new symlink is introduced in /etc,
  • a postinst leaves stray files in /var.

None of these break the build. All of them change device behaviour in the field.

Two modes​

The class is inherited only when one of the feature flags is set in PANTAVISOR_FEATURES:

FlagBehaviour on driftUse in
pv-manifest-auditPatch + WARNING (build proceeds)Dev distros, default in panta
pv-manifest-strictPatch + ERROR (build fails)Release / CI distros

pv-manifest-strict implies pv-manifest-audit. Setting both is fine — strict wins.

The default panta and panta-appengine distros enable advisory mode out of the box; flip to strict in a release config:

PANTAVISOR_FEATURES:append = " pv-manifest-strict"

Reference filename​

In this layer, references live in a pv-manifests/ directory next to the recipe (recipes-pv/images/pv-manifests/ for the initramfs, dynamic-layers/virtualization-layer/recipes-pv/images/pv-manifests/ for the appengine); downstream .bbappends conventionally use files/. Either way they are pulled into ${WORKDIR} via SRC_URI:

${PV_MANIFEST_PREFIX}_${DISTRO}-${MACHINE}-${DISTRO_CODENAME}.manifest.reference.txt

PV_MANIFEST_PREFIX is set per recipe (pv-initramfs for the initramfs image, pv-appengine for the appengine image). Concrete examples:

pv-initramfs_panta-raspberrypi-armv8-scarthgap.manifest.reference.txt
pv-initramfs_panta-bananapi-m2-berry-scarthgap.manifest.reference.txt
pv-appengine_panta-appengine-docker-x86_64-scarthgap.manifest.reference.txt

The underscore separates the recipe prefix from the distro/machine/codename triple — both halves can themselves contain dashes, so the underscore makes the boundary unambiguous. The -${DISTRO_CODENAME} segment is dropped when DISTRO_CODENAME is unset.

How to add a reference for a new MACHINE / DISTRO​

The reference is a normal SRC_URI file:// entry. Add it from this layer (for upstream-supported machines) or from a downstream .bbappend:

# pantavisor-initramfs.bbappend in your layer
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
SRC_URI += "file://pv-initramfs_panta-<MACHINE>-<CODENAME>.manifest.reference.txt"

Drop the file under <your-layer>/files/. Normal Yocto override semantics apply: FILESEXTRAPATHS:prepend in a bbappend wins over the upstream layer's copy.

Bootstrapping a brand-new MACHINE:

  1. Build with advisory mode (pv-manifest-audit set, no pv-manifest-strict).

  2. The build prints a full-add patch to the bitbake log and writes ${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}.manifest.patch (also copied to the final deploy/images/<MACHINE>/ directory).

  3. Apply the patch under your layer's reference directory (pv-manifests/ in this layer, files/ in a downstream bbappend):

    cd <layer>/files
    patch < <build>/tmp-scarthgap/.../pantavisor-initramfs-<MACHINE>-<TS>.manifest.patch
  4. Add the matching SRC_URI += "file://..." line and rebuild. The next do_rootfs should match cleanly.

Output artifacts​

Two files land in the image deploy dir for every build:

FileWhen written
${IMAGE_NAME}.manifest.txtAlways
${IMAGE_NAME}.manifest.patchOnly when there is a drift to record

Each is accompanied by a stable, build-id-free symlink (${IMAGE_LINK_NAME}.manifest.txt / .manifest.patch) pointing at the latest versioned file, so tooling can reference a fixed path.

The patch is also dumped in full into the bitbake log via bb.plain so it shows in CI output without any extra plumbing. Look for the banner:

=== pv-manifest-audit PATCH (<MACHINE>, audit|STRICT) ===
# patch file: tmp-<codename>/.../...manifest.patch (relative to TOPDIR)
# manifest: tmp-<codename>/.../...manifest.txt (relative to TOPDIR)
# apply with: cd <layer>/files && patch < $TOPDIR/<patch>
--- <reference name>
+++ <reference name>
...
=== end pv-manifest-audit PATCH ===

Manifest format​

# pv-manifest-audit v1
# format: type mode uid gid path[ -> symlink-target | major,minor]
# machine: <MACHINE>
# distro: <DISTRO> (<DISTRO_CODENAME>)
# prefix: <PV_MANIFEST_PREFIX>
# exclude: <space-separated paths excluded from the walk>
<entries, sorted by path>

Each entry is <type> <mode> <uid> <gid> <path>[<tail>] where tail is -> <target> for a symlink, <major>,<minor> for a device node, or empty. Types: f regular, d directory, l symlink, c char device, b block device, p fifo, s socket.

File size is intentionally omitted: it would diff on every dependency version bump (busybox, mbedtls, kernel modules) without reflecting a meaningful behavioural change. Adding or removing a file is still detected via its presence or absence.

Excludes​

PV_MANIFEST_EXCLUDES is a space-separated list of rootfs-relative path prefixes to skip. Defaults cover package-manager state files whose contents flip on every rebuild without indicating real change:

/var/lib/rpm
/var/lib/dnf
/var/lib/opkg
/usr/lib/opkg
/var/cache/ldconfig
/var/cache/dnf
/var/cache/yum

Package add/remove is still detectable via the actual installed files (binaries, libs, configs in /usr, /etc, etc.); only the package-manager bookkeeping is filtered.

A recipe or downstream layer can extend the list:

PV_MANIFEST_EXCLUDES:append = " /etc/ssh/ssh_host_keys /etc/machine-id"

The exclude list is part of the task's input hash (vardeps), so changes trigger a do_rootfs rerun.

Class variables​

VariableDefaultPurpose
PV_MANIFEST_PREFIX${PN}Recipe-stable label embedded in the reference filename
PV_MANIFEST_REFERENCE_NAME${PV_MANIFEST_PREFIX}_${DISTRO}-${MACHINE}-${DISTRO_CODENAME}.manifest.reference.txt (the -${DISTRO_CODENAME} segment is dropped when DISTRO_CODENAME is unset)Basename of the reference fetched into ${WORKDIR}
PV_MANIFEST_EXCLUDES(see above)Rootfs-relative path prefixes to omit from the manifest

Inheriting the class​

Recipes inherit the class conditionally so it is a no-op when neither feature flag is set:

inherit ${@bb.utils.contains_any('PANTAVISOR_FEATURES', \
'pv-manifest-audit pv-manifest-strict', 'pv-manifest-audit', '', d)}
PV_MANIFEST_PREFIX = "pv-initramfs" # or pv-appengine, etc.

Currently inherited by:

  • recipes-pv/images/pantavisor-initramfs.bb
  • dynamic-layers/virtualization-layer/recipes-pv/images/pantavisor-appengine.inc

Implementation​

classes/pv-manifest-audit.bbclass adds a dedicated do_pv_manifest_audit task that runs after do_rootfs and before do_image. It is marked [fakeroot], so it runs under pseudo and the uid/gid recorded in the manifest are the same ones that end up in the cpio / tarball.

The task is marked [nostamp], so it re-runs on every build. This is deliberate: a ROOTFS_POSTPROCESS_COMMAND only executes while do_rootfs itself runs, so once the rootfs stamp is valid or the image is restored from sstate via setscene, the gate would be silently skipped — a strict-mode deviation would fail the first build but pass every rebuild. As a nostamp task consuming the live IMAGE_ROOTFS, it always runs (and forces do_rootfs to produce a real rootfs), so a strict deviation keeps failing until the reference is updated.