fix(genbi): secure context loader installer paths

This commit is contained in:
Jax Liu
2026-09-04 21:55:11 +08:00
parent 19c18ac2f8
commit 65ef45bde6
2 changed files with 170 additions and 35 deletions
+126 -25
View File
@@ -1,24 +1,37 @@
import { createHash } from "node:crypto";
import { access, chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import { execFile as execFileCallback } from "node:child_process";
import { lstatSync } from "node:fs";
import { chmod, lstat, mkdir, open, readFile, realpath, rename, rm, stat } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { gunzipSync } from "node:zlib";
import { CANONICAL_BINARY, ContextLoaderPackageError, isSha256, readJson, sha256File, targetFor } from "../lib/verified.mjs";
import { CANONICAL_BINARY, ContextLoaderPackageError, isSha256, sha256File, targetFor } from "../lib/verified.mjs";
const execFile = promisify(execFileCallback);
const SOURCE_ORIGINS = new Set([
"git@github.com:Canner/WrenAI.git",
"https://github.com/Canner/WrenAI.git",
"ssh://git@github.com/Canner/WrenAI.git",
]);
/** Downloads, verifies, and atomically installs the exact manifest row for this package. */
export async function installContextLoader({ packageRoot, fetchImpl = fetch, platform = process.platform, arch = process.arch } = {}) {
if (typeof packageRoot !== "string") throw new TypeError("packageRoot is required");
const packageJson = readJson(path.join(packageRoot, "package.json"), "package.json");
const manifest = readJson(path.join(packageRoot, "artifacts.json"), "artifacts.json");
const root = await physicalPackageRoot(packageRoot);
const packageJson = await readOwnedJson(root, "package.json", "package.json");
const manifest = await readOwnedJson(root, "artifacts.json", "artifacts.json");
const target = targetFor(platform, arch);
const artifact = manifest.artifacts?.[target];
validateManifest(packageJson, manifest, artifact, target);
const binary = path.join(packageRoot, CANONICAL_BINARY);
const statePath = path.join(packageRoot, "install-state.json");
const reusable = await existingVerified(binary, statePath, packageJson.version, target, artifact.binarySha256);
if (reusable) return { binary, reused: true };
await ensureOwnedDirectory(root, "bin");
const reusable = await existingVerified(root, packageJson.version, target, artifact.archiveSha256, artifact.binarySha256);
if (reusable !== undefined) return { binary: reusable, reused: true };
const binary = path.join(root, CANONICAL_BINARY);
const statePath = path.join(root, "install-state.json");
const staging = `${binary}.tmp-${process.pid}-${Date.now()}`;
let stagingCreated = false;
try {
const response = await fetchImpl(artifact.url);
if (!response?.ok) throw new ContextLoaderPackageError("download-failed", `could not download the ${target} artifact`);
@@ -30,17 +43,24 @@ export async function installContextLoader({ packageRoot, fetchImpl = fetch, pla
if (sha256(content) !== artifact.binarySha256) {
throw new ContextLoaderPackageError("binary-digest-mismatch", `extracted ${target} binary did not match its SHA-256`);
}
await mkdir(path.dirname(binary), { recursive: true });
await writeFile(staging, content, { mode: 0o755 });
await ensureOwnedDirectory(root, "bin");
await writeNewOwnedFile(root, path.join("bin", path.basename(staging)), content, 0o755);
stagingCreated = true;
await chmod(staging, 0o755);
await ownedRegularFile(root, path.join("bin", path.basename(staging)));
await ensureOwnedDirectory(root, "bin");
await rename(staging, binary);
stagingCreated = false;
const installedBinary = await ownedRegularFile(root, CANONICAL_BINARY);
const state = { package: packageJson.name, version: packageJson.version, target, archiveSha256: artifact.archiveSha256, binarySha256: artifact.binarySha256, binaryPath: CANONICAL_BINARY };
const stateTmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
await writeFile(stateTmp, `${JSON.stringify(state)}\n`, { mode: 0o600 });
const stateName = `install-state.json.tmp-${process.pid}-${Date.now()}`;
const stateTmp = path.join(root, stateName);
await writeNewOwnedFile(root, stateName, `${JSON.stringify(state)}\n`, 0o600);
await rename(stateTmp, statePath);
return { binary, reused: false };
await ownedRegularFile(root, "install-state.json");
return { binary: installedBinary, reused: false };
} catch (error) {
await rm(staging, { force: true });
if (stagingCreated) await removeOwnedFile(root, path.join("bin", path.basename(staging)));
throw error instanceof ContextLoaderPackageError ? error : new ContextLoaderPackageError("download-failed", "could not download the required artifact (offline cache miss)");
}
}
@@ -51,14 +71,19 @@ export async function installContextLoader({ packageRoot, fetchImpl = fetch, pla
* packed packages always download and verify their tagged artifact.
*/
export async function isRepositorySourcePackage(packageRoot) {
const repositoryRoot = path.resolve(packageRoot, "..", "..");
if (path.resolve(packageRoot) !== path.join(repositoryRoot, "apps", "context-loader")) return false;
try {
await Promise.all([
access(path.join(repositoryRoot, ".git")),
access(path.join(repositoryRoot, "pnpm-workspace.yaml")),
access(path.join(repositoryRoot, "core", "wren", "pyproject.toml")),
]);
if (!lstatSync(packageRoot).isDirectory()) return false;
const physicalPackage = await realpath(packageRoot);
const { stdout: repositoryOutput } = await execFile("git", ["-C", physicalPackage, "rev-parse", "--show-toplevel"], { timeout: 2_000 });
const repositoryRoot = await realpath(repositoryOutput.trim());
if (physicalPackage !== path.join(repositoryRoot, "apps", "context-loader")) return false;
const { stdout: originOutput } = await execFile("git", ["-C", repositoryRoot, "remote", "get-url", "origin"], { timeout: 2_000 });
if (!SOURCE_ORIGINS.has(originOutput.trim())) return false;
await execFile(
"git",
["-C", repositoryRoot, "ls-files", "--error-unmatch", "--", "apps/context-loader/package.json", "pnpm-workspace.yaml", "core/wren/pyproject.toml"],
{ timeout: 2_000 },
);
return true;
} catch {
return false;
@@ -75,15 +100,91 @@ function validateManifest(packageJson, manifest, artifact, target) {
}
}
async function existingVerified(binary, statePath, version, target, digest) {
async function existingVerified(root, version, target, archiveDigest, binaryDigest) {
try {
const state = JSON.parse(await readFile(statePath, "utf8"));
return state.version === version && state.target === target && state.binaryPath === CANONICAL_BINARY && state.binarySha256 === digest && sha256File(binary) === digest;
const stateFile = await ownedRegularFile(root, "install-state.json", { allowMissing: true });
const binary = await ownedRegularFile(root, CANONICAL_BINARY, { allowMissing: true });
if (stateFile === undefined || binary === undefined) return undefined;
const state = JSON.parse(await readFile(stateFile, "utf8"));
return state.version === version && state.target === target && state.binaryPath === CANONICAL_BINARY && state.archiveSha256 === archiveDigest && state.binarySha256 === binaryDigest && sha256File(binary) === binaryDigest ? binary : undefined;
} catch {
return false;
throw new ContextLoaderPackageError("unsafe-path", "existing install state or binary is linked, malformed, or outside the package root");
}
}
async function physicalPackageRoot(packageRoot) {
try {
if (!(await lstat(packageRoot)).isDirectory()) throw new Error("not a directory");
return await realpath(packageRoot);
} catch {
throw new ContextLoaderPackageError("unsafe-path", "package root is missing, linked, or not a real directory");
}
}
async function readOwnedJson(root, relativePath, label) {
const file = await ownedRegularFile(root, relativePath);
try {
const value = JSON.parse(await readFile(file, "utf8"));
if (typeof value !== "object" || value === null) throw new Error("not an object");
return value;
} catch {
throw new ContextLoaderPackageError("invalid-manifest", `${label} is missing or invalid JSON`);
}
}
async function ensureOwnedDirectory(root, relativePath) {
const directory = path.join(root, relativePath);
try {
await mkdir(directory, { recursive: true });
} catch {
throw new ContextLoaderPackageError("unsafe-path", "could not create the package-local binary directory");
}
try {
if (!(await lstat(directory)).isDirectory()) throw new Error("not a directory");
if ((await realpath(directory)) !== directory) throw new Error("linked directory");
} catch {
throw new ContextLoaderPackageError("unsafe-path", "package-local binary directory is linked or outside the package root");
}
}
async function ownedRegularFile(root, relativePath, { allowMissing = false } = {}) {
const lexical = path.resolve(root, relativePath);
if (!lexical.startsWith(`${root}${path.sep}`)) throw new ContextLoaderPackageError("unsafe-path", "requested file escapes the package root");
try {
if (!(await lstat(lexical)).isFile()) throw new Error("not a regular file");
const physical = await realpath(lexical);
if (!physical.startsWith(`${root}${path.sep}`) || !(await stat(physical)).isFile()) throw new Error("not contained");
return physical;
} catch (error) {
if (allowMissing && isMissing(error)) return undefined;
throw new ContextLoaderPackageError("unsafe-path", "package-local file is linked, missing, or outside the package root");
}
}
async function writeNewOwnedFile(root, relativePath, content, mode) {
const lexical = path.resolve(root, relativePath);
if (!lexical.startsWith(`${root}${path.sep}`)) throw new ContextLoaderPackageError("unsafe-path", "staging file escapes the package root");
const handle = await open(lexical, "wx", mode);
try {
await handle.writeFile(content);
} finally {
await handle.close();
}
}
async function removeOwnedFile(root, relativePath) {
try {
const file = await ownedRegularFile(root, relativePath, { allowMissing: true });
if (file !== undefined) await rm(file, { force: true });
} catch {
// Never follow an unexpected link while cleaning up an interrupted install.
}
}
function isMissing(error) {
return typeof error === "object" && error !== null && error.code === "ENOENT";
}
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
+44 -10
View File
@@ -1,9 +1,11 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { gzipSync } from "node:zlib";
import { installContextLoader, isRepositorySourcePackage } from "../scripts/installer.mjs";
import { readVerifiedState } from "../lib/verified.mjs";
@@ -95,17 +97,49 @@ test("rejects final-file and ancestor-directory symlinks at runtime", async () =
assert.throws(() => readVerifiedState(ancestorLink.root), /linked|outside/);
});
test("skips only the verifiable source-workspace layout, never an equivalent packed copy", async () => {
const repositoryRoot = await mkdtemp(path.join(os.tmpdir(), "context-loader-source-"));
const sourcePackage = path.join(repositoryRoot, "apps", "context-loader");
await mkdir(path.join(repositoryRoot, "core", "wren"), { recursive: true });
await mkdir(sourcePackage, { recursive: true });
await writeFile(path.join(repositoryRoot, ".git"), "gitdir: fixture\n");
await writeFile(path.join(repositoryRoot, "pnpm-workspace.yaml"), "packages: []\n");
await writeFile(path.join(repositoryRoot, "core", "wren", "pyproject.toml"), "[project]\nname = \"wren\"\n");
test("refuses a same-bytes external binary during reuse and a bin-directory link before any install write", async () => {
const reusable = await fixture();
const installed = await installContextLoader({ packageRoot: reusable.root, fetchImpl: fetchArchive(reusable.archive), platform: "darwin", arch: "arm64" });
const external = path.join(await mkdtemp(path.join(os.tmpdir(), "context-loader-reuse-external-")), "wren-context-loader");
await writeFile(external, await readFile(installed.binary), { mode: 0o755 });
await rm(installed.binary);
await symlink(external, installed.binary);
await assert.rejects(
installContextLoader({ packageRoot: reusable.root, fetchImpl: fetchArchive(reusable.archive), platform: "darwin", arch: "arm64" }),
/unsafe-path/,
);
assert.deepEqual(await readFile(external), reusable.artifact);
const fresh = await fixture();
const externalDir = await mkdtemp(path.join(os.tmpdir(), "context-loader-write-external-"));
const sentinel = path.join(externalDir, "wren-context-loader");
await writeFile(sentinel, "outside-before", { mode: 0o755 });
await symlink(externalDir, path.join(fresh.root, "bin"));
await assert.rejects(
installContextLoader({ packageRoot: fresh.root, fetchImpl: fetchArchive(fresh.archive), platform: "darwin", arch: "arm64" }),
/unsafe-path/,
);
assert.equal(await readFile(sentinel, "utf8"), "outside-before");
assert.deepEqual(await readdir(externalDir), ["wren-context-loader"]);
});
test("skips only the tracked source checkout, never a lookalike git repository or packed copy", async () => {
const sourcePackage = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
assert.equal(await isRepositorySourcePackage(sourcePackage), true);
const packedCopy = path.join(repositoryRoot, "node_modules", "@wrenai", "context-loader");
const fakeRoot = await mkdtemp(path.join(os.tmpdir(), "context-loader-fake-source-"));
const fakePackage = path.join(fakeRoot, "apps", "context-loader");
await mkdir(path.join(fakeRoot, "core", "wren"), { recursive: true });
await mkdir(fakePackage, { recursive: true });
await writeFile(path.join(fakePackage, "package.json"), "{}\n");
await writeFile(path.join(fakeRoot, "pnpm-workspace.yaml"), "packages: []\n");
await writeFile(path.join(fakeRoot, "core", "wren", "pyproject.toml"), "[project]\nname = \"wren\"\n");
execFileSync("git", ["init", fakeRoot]);
execFileSync("git", ["-C", fakeRoot, "add", "apps/context-loader/package.json", "pnpm-workspace.yaml", "core/wren/pyproject.toml"]);
execFileSync("git", ["-C", fakeRoot, "remote", "add", "origin", "https://example.invalid/lookalike.git"]);
assert.equal(await isRepositorySourcePackage(fakePackage), false);
const packedCopy = path.join(fakeRoot, "node_modules", "@wrenai", "context-loader");
await mkdir(packedCopy, { recursive: true });
assert.equal(await isRepositorySourcePackage(packedCopy), false);
});