Keep checked browser runtime versions independent of authority upgrades

This commit is contained in:
Timothy J. Aveni
2026-10-07 17:03:01 -07:00
parent 391f02f78a
commit 61534d3fb5
3 changed files with 24 additions and 7 deletions
+18 -4
View File
@@ -10,10 +10,16 @@ import {
type CandidateMetadataScope, type CandidateMetadataScope,
} from "./metadata.js"; } from "./metadata.js";
const rootPattern = /^\/nix\/store\/[a-z0-9]{32}-[^/\s]+$/; const rootPattern = /^\/nix\/store\/[a-z0-9]{32}-[^/\s]+$/;
async function readJson(root: string, file: string, scope: CandidateMetadataScope): Promise<any> { async function readJson(root: string, file: string, scope: CandidateMetadataScope) {
const input = await readCandidateMetadata(join(root, file), scope, 32 * 1024 * 1024); const input = await readCandidateMetadata(join(root, file), scope, 32 * 1024 * 1024);
try { try {
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes)); return {
value: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes)),
releaseTree: input.releaseTree,
};
} catch (error) {
input.releaseTree();
throw error;
} finally { } finally {
input.releaseInput(); input.releaseInput();
} }
@@ -30,6 +36,7 @@ export async function loadCheckedInstallationCandidate(
if (!rootPattern.test(root) || !(await lstat(root)).isDirectory()) if (!rootPattern.test(root) || !(await lstat(root)).isDirectory())
throw Error("Candidate must be an immutable Nix output directory"); throw Error("Candidate must be an immutable Nix output directory");
const scope = (resources.budget ?? candidateMetadataBudget).acquire(resources.owner ?? root); const scope = (resources.budget ?? candidateMetadataBudget).acquire(resources.owner ?? root);
let releaseValidation = () => {};
try { try {
// Sequential reads avoid partially rejected Promise.all() work allocating // Sequential reads avoid partially rejected Promise.all() work allocating
// after the failed candidate's reservation has been released. // after the failed candidate's reservation has been released.
@@ -43,8 +50,13 @@ export async function loadCheckedInstallationCandidate(
"kernel.json", "kernel.json",
"queries.json", "queries.json",
"graph-execution.json", "graph-execution.json",
]) ]) {
values.push(await readJson(root, file, scope)); const parsed = await readJson(root, file, scope);
values.push(parsed.value);
// candidate.json duplicates the checked plan for validation. Nothing in
// the installed candidate retains this input tree after validation.
if (file === "candidate.json") releaseValidation = parsed.releaseTree;
}
const [candidate, plan, artifacts, source, entryRequest, kernel, queries, graph] = values; const [candidate, plan, artifacts, source, entryRequest, kernel, queries, graph] = values;
const { executionContracts: _legacyContracts, ...workspace } = candidate; const { executionContracts: _legacyContracts, ...workspace } = candidate;
if ( if (
@@ -149,6 +161,8 @@ export async function loadCheckedInstallationCandidate(
} catch (error) { } catch (error) {
scope.release(); scope.release();
throw error; throw error;
} finally {
releaseValidation();
} }
} }
+3 -3
View File
@@ -104,7 +104,7 @@ export async function readCandidateMetadata(
path: string, path: string,
scope: CandidateMetadataScope, scope: CandidateMetadataScope,
limit: number, limit: number,
): Promise<{ bytes: Uint8Array; releaseInput: () => void }> { ): Promise<{ bytes: Uint8Array; releaseInput: () => void; releaseTree: () => void }> {
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
let releaseInput: (() => void) | undefined; let releaseInput: (() => void) | undefined;
try { try {
@@ -123,8 +123,8 @@ export async function readCandidateMetadata(
if (offset !== stat.size) throw Error("CANDIDATE_METADATA_CHANGED"); if (offset !== stat.size) throw Error("CANDIDATE_METADATA_CHANGED");
const content = bytes.subarray(0, offset); const content = bytes.subarray(0, offset);
// No JSON.parse/decoded tree exists until its shape-based charge succeeds. // No JSON.parse/decoded tree exists until its shape-based charge succeeds.
scope.charge(jsonAllocation(content)); const releaseTree = scope.reserve(jsonAllocation(content));
return { bytes: content, releaseInput }; return { bytes: content, releaseInput, releaseTree };
} catch (error) { } catch (error) {
releaseInput?.(); releaseInput?.();
throw error; throw error;
+3
View File
@@ -48,6 +48,9 @@ test("metadata files reserve parse/index space before allocation and refuse over
await assert.rejects(readCandidateMetadata(join(root, "linked"), scope, 100), { code: "ELOOP" }); await assert.rejects(readCandidateMetadata(join(root, "linked"), scope, 100), { code: "ELOOP" });
await assert.rejects(readCandidateMetadata(root, scope, 100), /CANDIDATE_METADATA_FILE_LIMIT/); await assert.rejects(readCandidateMetadata(root, scope, 100), /CANDIDATE_METADATA_FILE_LIMIT/);
assert.equal(scope.bytes, first); assert.equal(scope.bytes, first);
input.releaseTree();
input.releaseTree();
assert.equal(scope.bytes, 0, "Discarded validation trees can release independently of the candidate");
} finally { } finally {
scope.release(); scope.release();
await rm(root, { recursive: true, force: true }); await rm(root, { recursive: true, force: true });