Skip to content

Commit 25c8345

Browse files
ShadowfetchShadowfetch
authored andcommitted
Refresh and validate pinned Alpine AppImage dependencies
1 parent eefc102 commit 25c8345

7 files changed

Lines changed: 175 additions & 18 deletions

File tree

‎packaging/native-runtime/README.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,24 @@ reproducibility across compiler build timestamps.
8080

8181
## Regression checks
8282

83+
Before changing APK pins, run `node scripts/check-native-runtime-pins.mjs`
84+
(optionally `--output /path/to/audit.json`). This read-only audit checks every
85+
exact pin against the official Alpine v3.21 `main` and `community` indexes for
86+
both `aarch64` and `x86_64`, and compares retained dependency recipes with the
87+
APKs' aports commits. Its index hashes record the observation; they do not
88+
replace APK signature or dependency verification in the pinned container.
89+
The audit never updates a pin automatically.
90+
91+
Packaging validates linked-library versions against `alpine-sources.json`
92+
before reusing a cached runtime or building a new one. The source-fetch tests
93+
also reject mismatched architecture overrides, floating or duplicate pins,
94+
and recipe URLs that do not identify the retained immutable aports commit.
95+
When a library version changes, update its archive, recipe hash and complete
96+
patch list together. For example, Alpine's zlib 1.4-r0 recipe at
97+
`d3332621d44c2c4cf52e52217b16674db190212c` has only the upstream archive;
98+
the separate `CVE-2026-85091.patch` from 1.3.2-r1 is no longer a build input.
99+
100+
83101
`node --test scripts/test-native-source-fetch.mjs` checks unexpected HTTP 200
84102
bodies, HTTP errors, redirects, identical-byte mirror recovery, both digest
85103
algorithms, retained-source reuse without HTTP requests, corrupt retained inputs,

‎packaging/native-runtime/alpine-sources.json‎

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -357,32 +357,27 @@
357357
},
358358
{
359359
"name": "zlib",
360-
"version": "1.3.2-r1",
361-
"aportsCommit": "c973d294927355eac1d2a6e97d86e41fbab8ffda",
360+
"version": "1.4-r0",
361+
"aportsCommit": "d3332621d44c2c4cf52e52217b16674db190212c",
362362
"license": "Zlib",
363-
"archive": "zlib-1.3.2.tar.gz",
364-
"sourceDirectory": "zlib-1.3.2",
363+
"archive": "zlib-1.4.tar.gz",
364+
"sourceDirectory": "zlib-1.4",
365365
"notices": [
366366
"LICENSE"
367367
],
368368
"files": [
369369
{
370370
"file": "APKBUILD",
371-
"url": "https://raw.githubusercontent.com/alpinelinux/aports/c973d294927355eac1d2a6e97d86e41fbab8ffda/main/zlib/APKBUILD",
372-
"sha512": "c1f60be4e44bb3f38fb4b5d44bcb408ec6c3d035d63829668435989adc674dc292c48656a0adf081a3eebcd33fff1da75db329778d117b9f2db94a4176b82e48"
371+
"url": "https://raw.githubusercontent.com/alpinelinux/aports/d3332621d44c2c4cf52e52217b16674db190212c/main/zlib/APKBUILD",
372+
"sha512": "6f16773e9362ca30c2e35e254b6a3b64433ea6554a59eefb4bf682b2cdabfe291a104afaa124dd85e5e0db73bc887f9ba76a79e530393c258a771e4a07578868"
373373
},
374374
{
375-
"file": "zlib-1.3.2.tar.gz",
376-
"url": "https://zlib.net/fossils/zlib-1.3.2.tar.gz",
375+
"file": "zlib-1.4.tar.gz",
376+
"url": "https://zlib.net/fossils/zlib-1.4.tar.gz",
377377
"mirrors": [
378-
"https://distfiles.alpinelinux.org/distfiles/v3.21/zlib-1.3.2.tar.gz"
378+
"https://distfiles.alpinelinux.org/distfiles/v3.21/zlib-1.4.tar.gz"
379379
],
380-
"sha512": "70963771ea5d763614278a69b474f09b7d237ef8f53b675a10fe31d9923aeef601504b35d7ebd1b1e7f347e9ebb048e6b3b47fffdf137e7bdc7e8d5eb4ec4692"
381-
},
382-
{
383-
"file": "CVE-2026-85091.patch",
384-
"url": "https://raw.githubusercontent.com/alpinelinux/aports/c973d294927355eac1d2a6e97d86e41fbab8ffda/main/zlib/CVE-2026-85091.patch",
385-
"sha512": "878888308a51296470f05eca59a79212314d4c9a26c19bbf70c1cb1b8d25eb42e0cce5a001acbe287268aa215c854cb5a9990ff62b7aefa9de221803d172957e"
380+
"sha512": "13f02285da85956ff722d814695b96807aff7a511ebf4b6d98cf708c353920666093cc18b6653cd7992c8e75c103f7b4baf2de3a105c2db9dcd62bab2c940859"
386381
}
387382
]
388383
},

‎packaging/native-runtime/apk-packages.txt‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ libcap-ng=0.8.5-r0
4949
libcap2=2.78-r0
5050
libcurl=8.14.1-r3
5151
libeconf=0.6.3-r0
52-
libexpat=2.9.0-r0
52+
libexpat=2.10.0-r0
5353
libfdisk=2.40.4-r1
5454
libffi=3.4.7-r0
5555
libgcc=14.2.0-r4
@@ -125,8 +125,8 @@ uuidgen=2.40.4-r1
125125
wipefs=2.40.4-r1
126126
xz=5.8.4-r0
127127
xz-libs=5.8.4-r0
128-
zlib-dev=1.3.2-r1
129-
zlib-static=1.3.2-r1
128+
zlib-dev=1.4-r0
129+
zlib-static=1.4-r0
130130
zstd=1.5.6-r2
131131
zstd-dev=1.5.6-r2
132132
zstd-libs=1.5.6-r2
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Read-only network audit: every exact pin on both supported Alpine builders.
2+
// This never updates locks. APK signature/dependency verification remains apk's
3+
// job in the pinned container; HTTPS index hashes here are audit evidence.
4+
import assert from "node:assert/strict";
5+
import { createHash } from "node:crypto";
6+
import { spawnSync } from "node:child_process";
7+
import { readFile, writeFile } from "node:fs/promises";
8+
import { runtimePins, validateRuntimeSourcePins, runtimeIndexError } from "./native-runtime-pins.mjs";
9+
const recipe = new URL("../packaging/native-runtime/", import.meta.url);
10+
const pins = await readFile(new URL("apk-packages.txt", recipe), "utf8");
11+
const sources = JSON.parse(await readFile(new URL("alpine-sources.json", recipe), "utf8"));
12+
validateRuntimeSourcePins(pins, sources);
13+
const outputIndex = process.argv.indexOf("--output");
14+
assert.ok(outputIndex === -1 || process.argv[outputIndex + 1], "--output requires a JSON path");
15+
const report = { checkedAt: new Date().toISOString(), branch: "v3.21", architectures: {} };
16+
let failures = 0;
17+
for (const arch of ["aarch64", "x86_64"]) {
18+
const records = new Map(), indexes = [];
19+
for (const repository of ["main", "community"]) {
20+
const url = `https://dl-cdn.alpinelinux.org/alpine/v3.21/${repository}/${arch}/APKINDEX.tar.gz`;
21+
const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
22+
assert.ok(response.ok, `${url}: HTTP ${response.status}`);
23+
const bytes = Buffer.from(await response.arrayBuffer());
24+
const tar = spawnSync("tar", ["-xzOf", "-", "APKINDEX"], { input: bytes, encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 15000 });
25+
assert.equal(tar.status, 0, tar.stderr || String(tar.error));
26+
indexes.push({ url, sha256: createHash("sha256").update(bytes).digest("hex") });
27+
for (const block of tar.stdout.trim().split(/\n\n/)) {
28+
const fields = Object.fromEntries(block.split("\n").filter(line => line.includes(":")).map(line => [line.slice(0, line.indexOf(":")), line.slice(line.indexOf(":") + 1)]));
29+
if (fields.P) records.set(`${fields.P}=${fields.V}`, fields);
30+
}
31+
}
32+
const selected = [];
33+
for (const [name, version] of runtimePins(pins, arch)) {
34+
const record = records.get(`${name}=${version}`);
35+
const error = runtimeIndexError(name, record, sources);
36+
if (error) failures++;
37+
selected.push({ name, version, origin: record?.o, aportsCommit: record?.c, error });
38+
}
39+
report.architectures[arch] = { indexes, pins: selected };
40+
console.log(`${arch}: ${selected.length} pins, ${selected.filter(pin => pin.error).length} errors`);
41+
for (const pin of selected.filter(pin => pin.error)) console.error(`${arch}: ${pin.name}=${pin.version}: ${pin.error}`);
42+
}
43+
if (outputIndex !== -1) await writeFile(process.argv[outputIndex + 1], `${JSON.stringify(report, null, 2)}\n`);
44+
assert.equal(failures, 0, "Runtime pins/source provenance require review; no lock was changed");

‎scripts/native-runtime-pins.mjs‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Validate the APK/source locks without selecting replacement versions.
2+
import assert from "node:assert/strict";
3+
4+
export function runtimePins(text, arch) {
5+
assert.ok(["aarch64", "x86_64"].includes(arch), `Unsupported APK architecture: ${arch}`);
6+
const base = new Map(), overrides = new Map();
7+
for (const line of text.trim().split(/\r?\n/)) {
8+
const match = /^(?:(aarch64|x86_64):)?([a-z0-9][a-z0-9+_.-]*)=([0-9][a-zA-Z0-9._+]*-r\d+)$/.exec(line);
9+
assert.ok(match, `APK pin must be exact: ${line}`);
10+
const [, target, name, version] = match;
11+
const pins = target ? overrides : base;
12+
const key = target ? `${target}:${name}` : name;
13+
assert.ok(!pins.has(key), `Duplicate APK pin: ${key}`);
14+
pins.set(key, version);
15+
}
16+
for (const [key, version] of overrides) {
17+
const [target, name] = key.split(":");
18+
assert.ok(base.has(name), `Override names no pinned package: ${key}`);
19+
if (target === arch) base.set(name, version);
20+
}
21+
return base;
22+
}
23+
24+
const linkedPackages = {
25+
gcc: ["gcc", "libgcc", "libstdc++", "libstdc++-dev"],
26+
musl: ["musl-dev"],
27+
mimalloc2: ["mimalloc2", "mimalloc2-dev", "mimalloc2-insecure"],
28+
zlib: ["zlib-dev", "zlib-static"],
29+
zstd: ["zstd", "zstd-dev", "zstd-libs", "zstd-static"],
30+
};
31+
32+
export function validateRuntimeSourcePins(text, sources) {
33+
assert.equal(sources.schema, 1);
34+
assert.deepEqual(sources.packages.map(p => p.name).sort(), Object.keys(linkedPackages).sort(), "Every linked Alpine dependency needs retained sources");
35+
for (const arch of ["aarch64", "x86_64"]) {
36+
const pins = runtimePins(text, arch);
37+
for (const source of sources.packages) {
38+
for (const name of linkedPackages[source.name])
39+
assert.equal(pins.get(name), source.version, `${arch}: ${name} differs from retained ${source.name} sources`);
40+
assert.match(source.aportsCommit, /^[a-f0-9]{40}$/);
41+
const names = source.files.map(file => file.file);
42+
assert.equal(new Set(names).size, names.length, `${source.name}: duplicate source file`);
43+
assert.ok(names.includes("APKBUILD") && names.includes(source.archive), `${source.name}: recipe and archive required`);
44+
for (const file of source.files) {
45+
assert.match(file.sha512, /^[a-f0-9]{128}$/);
46+
assert.ok(!/[\\/]/.test(file.file) && file.file !== "..", "Safe retained source filename");
47+
const url = new URL(file.url);
48+
assert.equal(url.protocol, "https:");
49+
if (file.file === "APKBUILD" || url.hostname === "raw.githubusercontent.com") {
50+
assert.equal(url.hostname, "raw.githubusercontent.com");
51+
assert.ok(url.pathname.startsWith(`/alpinelinux/aports/${source.aportsCommit}/`), `${source.name}: recipe/patch must match its immutable aports commit`);
52+
}
53+
}
54+
}
55+
}
56+
}
57+
58+
// Expected provenance comes from our lock mapping, never from untrusted index
59+
// metadata: an absent/wrong origin must not turn a linked-library check off.
60+
export function runtimeIndexError(name, record, sources) {
61+
if (!record) return "exact pin missing from official indexes";
62+
const expected = Object.entries(linkedPackages).find(([, packages]) => packages.includes(name))?.[0];
63+
if (!expected) return null; // Build tools only require the exact available pin.
64+
if (record.o !== expected) return `APK origin must be ${expected}`;
65+
const source = sources.packages.find(source => source.name === expected);
66+
if (!source || source.aportsCommit !== record.c) return "retained recipe differs from APK build commit";
67+
return null;
68+
}

‎scripts/native-runtime.mjs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { fileURLToPath } from "node:url";
1616
import { promisify } from "node:util";
1717
import { applyPackagingPath } from "./native-packaging-env.mjs";
1818
import { archInfo, hostArch } from "./native-arch.mjs";
19+
import { validateRuntimeSourcePins } from "./native-runtime-pins.mjs";
1920
const root = fileURLToPath(new URL("../", import.meta.url));
2021
const exec = promisify(execFile);
2122
const sha = (value) => createHash("sha256").update(value).digest("hex");
@@ -50,6 +51,10 @@ export async function buildRuntime(arch = hostArch().arch) {
5051
const info = archInfo(arch);
5152
applyPackagingPath(root);
5253
const recipe = path.join(root, "packaging/native-runtime");
54+
validateRuntimeSourcePins(
55+
await readFile(path.join(recipe, "apk-packages.txt"), "utf8"),
56+
JSON.parse(await readFile(path.join(recipe, "alpine-sources.json"), "utf8")),
57+
);
5358
const files = [
5459
"manifest.json",
5560
"Dockerfile",

‎scripts/test-native-source-fetch.mjs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,30 @@ test("source retrieval verifies every candidate and reuses retained bytes", asyn
113113
await rm(directory, { recursive: true, force: true });
114114
}
115115
});
116+
117+
// These checks run before the expensive native packaging phase in existing CI.
118+
test("runtime APK pins and retained source provenance agree on both architectures", async () => {
119+
const { runtimePins, validateRuntimeSourcePins, runtimeIndexError } = await import("./native-runtime-pins.mjs");
120+
const recipe = new URL("../packaging/native-runtime/", import.meta.url);
121+
const pins = await readFile(new URL("apk-packages.txt", recipe), "utf8");
122+
const sources = JSON.parse(await readFile(new URL("alpine-sources.json", recipe), "utf8"));
123+
validateRuntimeSourcePins(pins, sources);
124+
assert.throws(() => validateRuntimeSourcePins(pins.replace("zlib-static=1.4-r0", "zlib-static=1.3.2-r1"), sources), /differs from retained zlib/);
125+
assert.throws(() => validateRuntimeSourcePins(`${pins}aarch64:zlib-static=1.3.2-r1\n`, sources), /aarch64.*differs from retained zlib/);
126+
const changed = structuredClone(sources);
127+
changed.packages.find(p => p.name === "zlib").files[0].url = "https://raw.githubusercontent.com/alpinelinux/aports/master/main/zlib/APKBUILD";
128+
assert.throws(() => validateRuntimeSourcePins(pins, changed), /immutable aports commit/);
129+
assert.throws(() => runtimePins("zlib-static>=1.4-r0", "aarch64"), /must be exact/);
130+
assert.throws(() => runtimePins("zlib-static=1.*-r0", "aarch64"), /must be exact/);
131+
assert.throws(() => runtimePins("zlib-static=1.4-r0\nzlib-static=1.4-r0", "x86_64"), /Duplicate/);
132+
assert.throws(() => runtimePins("aarch64:zlib-static=1.4-r0", "aarch64"), /no pinned package/);
133+
const zlib = sources.packages.find(p => p.name === "zlib");
134+
assert.equal(runtimeIndexError("zlib-static", { o: "zlib", c: zlib.aportsCommit }, sources), null);
135+
assert.match(runtimeIndexError("zlib-static", { c: zlib.aportsCommit }, sources), /origin must be zlib/);
136+
assert.match(runtimeIndexError("zlib-static", { o: "unexpected", c: zlib.aportsCommit }, sources), /origin must be zlib/);
137+
assert.match(runtimeIndexError("zlib-static", { o: "zlib", c: "0".repeat(40) }, sources), /recipe differs/);
138+
assert.match(runtimeIndexError("zlib-static", undefined, sources), /exact pin missing/);
139+
assert.equal(runtimeIndexError("bash", {}, sources), null, "Unlinked build tools remain availability-only");
140+
assert.equal(zlib.version, "1.4-r0");
141+
assert.deepEqual(zlib.files.map(f => f.file), ["APKBUILD", "zlib-1.4.tar.gz"], "Alpine 1.4 recipe no longer applies the old CVE patch");
142+
});

0 commit comments

Comments
 (0)