Skip to content

A cross-major build preflight would catch what one-major gating cannot — and the stamp guard fails open for it #1219

Description

@OffgridwithJD

What

A wrong-variable substitution can type-check on one major and fail only on
another. @jdatcmd substituted rel for grel while working on #1213: it
compiled on PG 18 and failed on PG 17. Nothing else in the pipeline can see
that
— not review, not the suite (which runs on one major during feature work),
not the .so fingerprint, which only says the binary matches the source it was
built from.

A build-only preflight across all supported majors closes it, and it costs a
compile rather than a cluster.

What it costs, measured

On pgcolumnar-audit, PG 15a/16a/17a/18a/19a, make -j8, no install and no
cluster:

pg15a   5s      pg16a   6s      pg17a   6s
pg18a   2s      pg19a   4s
TOTAL          23s

Twenty-three seconds to compile the tree against all five majors.

The obvious implementation is a trap, and I fell into it while measuring this

My first loop was make clean; PG_CONFIG=pg<N>a make per major. It recompiles —
35 objects each time, so the clean works — and it leaves the tree holding the
objects of the last major built
. The very next suite run, for PG 18, died:

FATAL:  could not load library ".../pg18a/lib/postgresql/pgcolumnar.so":
        undefined symbol: build_simple_rel_hook
FATAL: no cluster of our own on port 27095 after 8 attempts

build_simple_rel_hook is the PG 19 name. The loop ended on pg19a, and the
suite inherited its objects.

Why the existing guard did not catch it

lib.sh already has one — "Objects from another major link but do not load
(#536)" — and it is defeated here:

pgc_build_needs_clean() {
    ...
    [ "$have" = "$want" ] && echo no || echo yes
}

have is the build stamp: which major the harness last built. want is the
current major. Neither is a property of the objects in the tree. A hand-run
make leaves foreign objects and never touches the stamp, so have = 18 and
want = 18 agree while src/*.o are PG 19's, the clean is skipped, make finds
everything up to date, and PG 19 objects get installed into the PG 18 prefix.

The stamp fails closed in one direction and open in the other. A stale or
absent stamp produces a refusal — FATAL: the binary under test was not built from this source — which is safe and which I hit twice today. A stamp that
matches while the objects are foreign produces a silent wrong install. Only the
second direction is dangerous, and it is the one a cross-major preflight creates.

Shape of the work

  1. The preflight itself: compile against every major, report per major, no
    install, no cluster. 23s.
  2. Leave the tree as it was found. The preflight must remove objects between
    majors and rebuild the working major afterwards, or build each major in its
    own directory so the shared tree is never touched.
  3. Close the guard's open direction, which is worth doing whether or not the
    preflight lands: derive have from the objects rather than from a stamp the
    harness writes. A .o's major is recoverable without trusting anyone's
    bookkeeping, and any check that depends on a hand-written record is defeated by
    a hand-run command.

Item 3 is the one with value beyond this feature: it is the reason a preflight
would be safe to run at all, and today it is the reason a plain make in the
wrong order silently breaks the next suite.

Raised with @jdatcmd, who offered to take the implementation; I have written it
up and am happy either way round.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions