Skip to content

fix(image): fold permissions into the OS build id so they OTA - #208

Open
nicksinas wants to merge 1 commit into
mainfrom
nsinas/permission-build-id
Open

fix(image): fold permissions into the OS build id so they OTA#208
nicksinas wants to merge 1 commit into
mainfrom
nsinas/permission-build-id

Conversation

@nicksinas

@nicksinas nicksinas commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

ENG-2437

Problem

A rootfs change that only edits permissions: (users/groups/passwords) builds fine locally but never lands on a device over avocado deploy. The device logs:

OS already at target version (AVOCADO_OS_BUILD_ID=…), skipping OS bundle download

…and no-ops.

Root cause

AVOCADO_OS_BUILD_ID is a pure function of the rootfs rpmdbuuid5(namespace, sha256(sorted NEVRA of every installed RPM)). The permissions: section rewrites /etc/{passwd,shadow,group} in the image work dir but never touches the package DB, so an identical package set yields an identical build id regardless of the auth-file bytes. The deploy-side gate compares build ids and short-circuits both the download and the apply of the OS bundle, so the change silently never ships.

(Same class of blind spot existed for the initramfs build id.)

Fix

Fold a hash of the assembled auth files into the build id alongside the NEVRA hash:

  • Rootfs: uuid5(ns, "$PKG_HASH:$AUTH_HASH")
  • Initramfs: uuid5(ns, "$INITRAMFS_PKG_HASH:$INITRAMFS_AUTH_HASH")

AUTH_HASH is produced by a new shared helper, render_auth_files_hash, that lives beside the existing render_hook_block / resolve_install_hooks / render_users_groups_script helpers and is imported by initramfs::image — so rootfs and initramfs derive their build id the exact same way, from one implementation.

AUTH_HASH=$(cat \
    "$ROOTFS_WORK/etc/passwd" \
    "$ROOTFS_WORK/etc/shadow" \
    "$ROOTFS_WORK/etc/group" \
    "$ROOTFS_WORK/etc/gshadow" 2>/dev/null | LC_ALL=C sort | sha256sum | awk '{print $1}')
  • LC_ALL=C sort makes the digest independent of the order the permissions section appends entries in (the users:/groups: maps have no guaranteed iteration order), so it's fully deterministic.
  • 2>/dev/null tolerates a missing gshadow (minimal images) — absent files contribute nothing rather than aborting.
  • Computed after the permissions section runs, so it observes the change.

Why this doesn't cause spurious updates

The id is a content hash: an unchanged permissions: block hashes identically → same id → no OTA. It moves if and only if the auth-file content actually changes. This is the minimal correct mechanism — the deploy gate reads nothing but AVOCADO_OS_BUILD_ID, so a permissions-only change has to move the id to land.

One-time migration note

Because the build-id formula changes ($PKG_HASH$PKG_HASH:$AUTH_HASH), every existing image re-ids once on the first rebuild after this lands, triggering a single OS OTA even when nothing else changed. Steady state is churn-free thereafter.

Tests

  • test_render_auth_files_hash_snippet — exact-output unit test on the pure helper + reuse under the initramfs var names.
  • test_build_id_folds_auth_files (rootfs and initramfs) — structural + ordering assertions (auth hash computed after the permissions section, folded into the uuid5 input).
  • Full lib suite: 1452 passing; cargo fmt / cargo clippy clean on the changed files.

Scope / follow-up

This is the correctness fix only. A separate --force override for avocado deploy (an escape hatch for any future rootfs change that doesn't move the id) was intentionally split out into its own PR — this fix largely subsumes the permissions: case, so --force is a nice-to-have rather than a dependency.

Fixes ENG-2437.

A rootfs change that only edits `permissions:` (users/groups/passwords)
builds fine but never lands over `avocado deploy` — the device logs
`OS already at target version, skipping OS bundle download` and no-ops.

AVOCADO_OS_BUILD_ID is a pure function of the rootfs rpmdb (uuid5 over
the sorted NEVRA set), and `permissions:` rewrites /etc/{passwd,shadow,
group} without touching the package DB. So an identical package set
produced an identical build id regardless of the auth-file bytes, and
the deploy-side gate short-circuited the update.

Fold a hash of the assembled auth files into the build id alongside the
NEVRA hash, via a shared `render_auth_files_hash` helper used by both the
rootfs and initramfs build scripts. The hash is `LC_ALL=C`-sorted so it
is independent of the order the permissions section appends entries in,
and tolerates a missing gshadow. Any permissions change now moves the id
and therefore OTAs; an unchanged block hashes identically, so there is no
spurious update churn.

Note: because the build-id formula changes, every existing image re-ids
once on the first rebuild after this lands, triggering a single OS OTA
even when nothing else changed. Steady state is churn-free thereafter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant