Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/base-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ on:
- .github/workflows/base-image.yml
workflow_dispatch:

# One publication of `:latest` at a time, the newest queued last: two merges a
# minute apart started two builds, and the older one finished second and put
# its image back over the newer one.
concurrency:
group: publish-base-image
cancel-in-progress: false

permissions: {}

jobs:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ jobs:
docker:
needs: test
runs-on: ubuntu-latest
# Pushes `:latest` on main; two pushes racing would let the older image win.
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
packages: write
Expand Down
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,21 @@ scope. Add yours.
suite hung. The close had been the only thing ending it. β†’ Before removing an
exit from a loop, find what else that exit was the bound for, and test the
loop with a medium that never waits.
- **`ls a b` used as "does either exist".** A probe for a library in `lib` or
`lib64` listed both; `ls` fails when either is missing, so a library in `lib`
alone was never seen and the rebuild it was meant to trigger never ran. The
end-to-end scenario caught it; the unit tests had not been written yet. β†’ In
shell, test existence with `[ -e ]` per path, and write the unit test for a
probe with each of its branches before the scenario that relies on it.
- **A test edited from memory of it.** A replacement was written for a cache
test the agent had not opened, and matched nothing; the half of the edit that
did apply left the suite red for a reason that looked like the code's. β†’
Read the test before writing its replacement, every time.
- **A path read relative to the current directory in a script that is
sourced.** `start.sh` read `scripts/liboqs.lock` relative to wherever it had
`cd`'d, which under the test harness was `/usr/bin`; every stamp then looked
wrong. β†’ A file that decides trust is read from the script's own tree
(`BASH_SOURCE`), never from the current directory.

### Running the tests (read this before trusting "green")

Expand Down
35 changes: 29 additions & 6 deletions BUGSVULNS.MD
Original file line number Diff line number Diff line change
Expand Up @@ -3712,7 +3712,10 @@ packages come from a hash-locked set (`docker/requirements.lock`) audited weekly

## 93 β€” medium β€” A liboqs built before the lock, or left in a cache, is reused unproven

> **Status β€” open.**
> **Status β€” fixed.** Tests: `tests/test_start_script.py::TestTheProvenLibrary`,
> `tests/test_liboqs_lock.py::TestTheBuildStamps`; five scenarios of
> `start.sh` run end to end in a clean Debian container (fresh, stamped,
> legacy, legacy with no way to fetch, unproven cache).

**Where** β€” `start.sh`: `adopt_liboqs`, `liboqs_candidates`, `cache_liboqs`.

Expand All @@ -3723,11 +3726,12 @@ so does one copied from a cache or a prefix another install left β€” including
`~/.cache/nmesh`, which the account running the installer can write and a later
system install may adopt.

**What it would take** β€” a stamp next to each build naming the lock it was
built from (commit and digest), written only by `build_liboqs.sh`, and refusal to
adopt anything without the current one; existing nodes would then rebuild once,
which on a small machine is minutes of downtime at the next start, so when that
happens is a decision.
**Fixed** β€” `build_liboqs.sh` writes `nmesh-liboqs.lock` beside every build it
proved; `start.sh` keeps, caches and adopts only a library carrying the current
one, owned by this account or root and writable by nobody else. A library in
the prefix without it is rebuilt at the next start, beside the old one, and
swapped in only once the new one loads: a node that cannot rebuild keeps what it
has and says so. Existing nodes pay the compile once, at their next start.

## 94 β€” high β€” The published image shipped 260 fixable HIGH/CRITICAL vulnerabilities

Expand Down Expand Up @@ -3835,3 +3839,22 @@ builds and then cannot import. The test written for exactly this failed on
3.14. On 3.14 Trivy found four fixable HIGHs, all in pip's own vendored
packages, so the running image no longer carries pip. Unit and integration
suites pass on 3.14; CI's interpreter is now 3.14 (`TEST.md`, `AGENTS.md`).

## 100 β€” medium β€” Two builds raced to publish the base image, and the older won

> **Status β€” fixed.** Tests: `tests/test_workflows_pinned.py`
> (`test_what_publishes_latest_does_it_one_run_at_a_time`).

**Where** β€” `.github/workflows/base-image.yml`; the `docker` job of `ci.yml`
pushes the application image's `:latest` the same way.

**What happens** β€” merging two PRs that both touched the base image a minute
apart started two builds. The one from the older commit finished second, so
`nmesh-base:latest` went back to a build from before liboqs was compiled from a
checked source, without `/opt/liboqs`; every application build after it failed
on the missing directory. Nothing in the image says which commit it came from,
so the only symptom was that later builds broke.

**Fixed** β€” both publishers run in a concurrency group that never cancels a
run in progress, so they finish in the order they were queued and the newest
commit publishes last. The base was republished from `main` by hand.
17 changes: 14 additions & 3 deletions Docs/Setup/guide
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,20 @@ recompiling. `start.sh` also looks at the prefixes `install.sh` points it at
(`OQS_REUSE_FROM`, a `:`-separated list): the installer's home, their old user
installation, `/opt/nmesh`.

Reuse is never decided on a version number or a file name: the wrapper actually
**loads** the candidate library and both required algorithms answer, or we
recompile. After the copy, the check is run again **at the destination**. A
Reuse is never decided on a version number or a file name: the candidate must
carry **`nmesh-liboqs.lock`**, the stamp `scripts/build_liboqs.sh` writes beside
every build it proved, matching the lock in this tree; it must belong to this
account or to root and be writable by no group and no one else (the stamp is a
file like any other); and the wrapper must actually **load** it with both
required algorithms answering. After the copy, the check is run again **at the
destination**.

A library already in the prefix with no current stamp β€” every one built before
the source was checked β€” is **rebuilt** at the next start: beside the old one,
swapped in only once the new one loads, so a node that cannot rebuild (no
network, no compiler) keeps what it has, says so, and tries again next time.
A library the system provides (a distribution's package, Homebrew) carries no
stamp and needs none. A
wrapper version bump changes the cache key and therefore forces a real rebuild β€”
wrapper and library must stay paired.

Expand Down
Loading
Loading