The Ur Project

Documenting the craft of stacking things so they remain stacked. For a very long time.

pkg-verify(1)

NAME

pkg-verify - verify the on-disk state of installed packages

SYNOPSIS

pkg verify [options] name...

pkg verify [options] --all

pkg check [options] name...

DESCRIPTION

Files change after they're installed. Somebody hand-edits a binary to test a fix and forgets about it, a disk write fails halfway, or someone who shouldn't have been on the box replaced something. The package manager is the one thing on the system that knows what every file is supposed to be, so it should be able to tell you.

That's what this does. Every installed manifest lists every file its package shipped, with its type, size, permission bits and sha256. pkg verify walks that list and compares each entry against what's actually on disk.

Each name is a bare name, which verifies every installed subpackage of it, name/subpkg for just one, or +set. --all verifies everything installed.

Verifying a set

A set is verified by its members, not by the set itself: pkg verify +build-sys checks every installed member of +build-sys, nested sets included, the way pkg install would expand it. A member that isn't installed, or that no repo carries, is skipped with a note on stderr. It's not a failure, because verify checks what's on disk, not what should be. A set none of whose members is installed is an error.

Expanding a set reads the repo chain, which a plain name never does. What's verified is the members the set names, not the packages they depend on.

What gets checked

Each file gets one verdict, and the first check that fails decides it.

  • A regular file: that it exists, that it's a regular file, its size, its permission bits (setuid and friends included), and then its sha256.
  • A symlink: that it exists, that it's a symlink, and where it points.
  • A directory: that it exists and is a directory.

Ownership and timestamps aren't compared. Manifests don't record timestamps at all, on purpose: if you want to know whether a file changed, you have to read its contents. A timestamp can't tell you that.

What it doesn't tell you

Whether the manifest itself can be trusted. Verify takes the installed manifest as the truth and checks the disk against it. It catches drift, accidental or not, but it doesn't verify any signature. That's pkg-verify-sig(1), and for a published build, pkg-provenance(1).

Run it as root

Some files are only readable by root. When a file can't be read, verify can't say anything about it, so it reports io instead of guessing. If every failure was an unreadable file, the error says so and suggests running it again as root, rather than pointing you at a repair that won't help.

OPTIONS

--root DIR
The filesystem root that the package's absolute paths are checked under. Defaults to /, the live system. Point it at a staged tree or a mounted image to verify it offline.
--all
Verify every installed package. Takes no name.
--quiet
Only report the files that fail. The ok lines, and the header of a package with nothing failing, are left out.

All global options apply; see pkg(1).

OUTPUT

One block per subpackage, one line per file:

gzip/bin ok f /usr/bin/gzip ok l /usr/bin/gunzip FAIL sha256 /usr/bin/zcat FAIL missing /usr/share/man/man1/gzip.1

The token after ok is the file type: f, l or d. The token after FAIL says why:

missing
Not there at all.
type
There, but not the expected kind of thing.
mode
Permission bits differ.
size
Size differs.
sha256
Same size, different content.
target
The symlink points somewhere else.
io
Couldn't be read, so no verdict.

When anything failed, the command exits non-zero and tells you what to do next:

error: verify: 2 of 47 files failed verification; run 'pkg repair gzip' to restore them from the package

When more than one package failed, it names each one:

error: verify: 5 of 63727 files failed verification; run 'pkg repair <name>' to restore them from the package, for each of: nginx sudo

A package whose only failures are io isn't named, since repair won't help there.

When every failure was unreadable:

error: verify: 3 of 47 files could not be read (permission?); re-run as root

JSON

{ "ok": false, "checked": 47, "failed": 2, "skipped": [], "packages": [ { "package": "gzip/bin", "problems": [ "FAIL sha256 /usr/bin/zcat", ... ] } ] }

Here ok is the verification result, not whether the command ran. Only failing lines go in problems. skipped carries the set members that weren't verified, one note each, the same lines stderr gets.

EXAMPLES

One package, or one subpackage

pkg verify gzip pkg verify gzip --quiet pkg verify gzip/bin

Several at once

pkg verify gzip coreutils openssh/bin --quiet

A set

pkg verify +build-sys --quiet

Everything installed

pkg verify --all --quiet

Just the packages that failed:

pkg --json verify --all | jq -r '.packages[] | select(.problems != []) | .package'

A mounted image

You need both flags. --state-root picks the package database, and --root picks the filesystem the paths are checked in:

pkg --state-root /mnt/image/var/lib/pkg \ verify coreutils --root /mnt/image

A composed root

pkg --state-root /srv/roots/minimal/var/lib/pkg \ verify coreutils --root /srv/roots/minimal

Just the numbers

pkg --json verify coreutils | jq '{checked, failed}'

Just the paths that drifted

pkg --json verify coreutils \ | jq -r '.packages[].problems[]' | awk '{print $3}'

Config drift, or tampering?

A changed binary is worth a closer look. A changed config file usually isn't. That's what config files are for. So filter those out first:

pkg verify openssh --quiet | grep -v ' /etc/'

Fixing what it found

pkg repair gzip --dry-run pkg repair gzip pkg verify gzip --quiet

Watching it work

pkg -v verify coreutils pkg -vvv verify coreutils 2>&1 | grep FAIL

EXIT STATUS

0 when every file verified. 1 when any file failed, when a name isn't installed, or when no member of a named set is.

SEE ALSO

pkg(1), pkg-repair(1), pkg-query(1), pkg-verify-sig(1), pkg-provenance(1), pkg-state(5), pkg-subpkgs(7)