paseo-agy-acp 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +296 -0
- package/LICENSE +201 -0
- package/README.md +571 -0
- package/README.zh-CN.md +540 -0
- package/assets/official-kernel-compat/rc01/paseo_model_compat.py +172 -0
- package/dist/ACP Connector/acp/session/paseo-context.d.ts +6 -0
- package/dist/ACP Connector/acp/session/paseo-context.js +83 -0
- package/dist/ACP Connector/acp/session/paseo-context.js.map +1 -0
- package/dist/ACP Connector/acp/session/turn-scheduler.d.ts +90 -0
- package/dist/ACP Connector/acp/session/turn-scheduler.js +247 -0
- package/dist/ACP Connector/acp/session/turn-scheduler.js.map +1 -0
- package/dist/ACP Connector/admission/dispatch-boundary.d.ts +19 -0
- package/dist/ACP Connector/admission/dispatch-boundary.js +16 -0
- package/dist/ACP Connector/admission/dispatch-boundary.js.map +1 -0
- package/dist/ACP Connector/admission/errors.d.ts +18 -0
- package/dist/ACP Connector/admission/errors.js +46 -0
- package/dist/ACP Connector/admission/errors.js.map +1 -0
- package/dist/ACP Connector/admission/owner-instance.d.ts +31 -0
- package/dist/ACP Connector/admission/owner-instance.js +105 -0
- package/dist/ACP Connector/admission/owner-instance.js.map +1 -0
- package/dist/ACP Connector/admission/runtime.d.ts +20 -0
- package/dist/ACP Connector/admission/runtime.js +157 -0
- package/dist/ACP Connector/admission/runtime.js.map +1 -0
- package/dist/ACP Connector/admission/startup-recovery.d.ts +22 -0
- package/dist/ACP Connector/admission/startup-recovery.js +129 -0
- package/dist/ACP Connector/admission/startup-recovery.js.map +1 -0
- package/dist/ACP Connector/admission/turn-coordinator.d.ts +57 -0
- package/dist/ACP Connector/admission/turn-coordinator.js +369 -0
- package/dist/ACP Connector/admission/turn-coordinator.js.map +1 -0
- package/dist/ACP Connector/agent.d.ts +4 -0
- package/dist/ACP Connector/agent.js +6 -0
- package/dist/ACP Connector/agent.js.map +1 -0
- package/dist/ACP Connector/main.d.ts +2 -0
- package/dist/ACP Connector/main.js +31 -0
- package/dist/ACP Connector/main.js.map +1 -0
- package/dist/ACP Connector/official-kernel/admission-fence.d.ts +10 -0
- package/dist/ACP Connector/official-kernel/admission-fence.js +52 -0
- package/dist/ACP Connector/official-kernel/admission-fence.js.map +1 -0
- package/dist/ACP Connector/official-kernel/context.d.ts +3 -0
- package/dist/ACP Connector/official-kernel/context.js +52 -0
- package/dist/ACP Connector/official-kernel/context.js.map +1 -0
- package/dist/ACP Connector/official-kernel/errors.d.ts +5 -0
- package/dist/ACP Connector/official-kernel/errors.js +39 -0
- package/dist/ACP Connector/official-kernel/errors.js.map +1 -0
- package/dist/ACP Connector/official-kernel/identity.d.ts +3 -0
- package/dist/ACP Connector/official-kernel/identity.js +17 -0
- package/dist/ACP Connector/official-kernel/identity.js.map +1 -0
- package/dist/ACP Connector/official-kernel/json-rpc.d.ts +34 -0
- package/dist/ACP Connector/official-kernel/json-rpc.js +26 -0
- package/dist/ACP Connector/official-kernel/json-rpc.js.map +1 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-lifecycle.d.ts +115 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-lifecycle.js +1904 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-lifecycle.js.map +1 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-pins.d.ts +14 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-pins.js +29 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-pins.js.map +1 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-rc01-recipe.d.ts +2 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-rc01-recipe.js +32 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-rc01-recipe.js.map +1 -0
- package/dist/ACP Connector/official-kernel/kernel.d.ts +3 -0
- package/dist/ACP Connector/official-kernel/kernel.js +12 -0
- package/dist/ACP Connector/official-kernel/kernel.js.map +1 -0
- package/dist/ACP Connector/official-kernel/login.d.ts +1 -0
- package/dist/ACP Connector/official-kernel/login.js +73 -0
- package/dist/ACP Connector/official-kernel/login.js.map +1 -0
- package/dist/ACP Connector/official-kernel/mcp-rewrite.d.ts +1 -0
- package/dist/ACP Connector/official-kernel/mcp-rewrite.js +39 -0
- package/dist/ACP Connector/official-kernel/mcp-rewrite.js.map +1 -0
- package/dist/ACP Connector/official-kernel/mode-map.d.ts +5 -0
- package/dist/ACP Connector/official-kernel/mode-map.js +35 -0
- package/dist/ACP Connector/official-kernel/mode-map.js.map +1 -0
- package/dist/ACP Connector/official-kernel/ndjson.d.ts +3 -0
- package/dist/ACP Connector/official-kernel/ndjson.js +29 -0
- package/dist/ACP Connector/official-kernel/ndjson.js.map +1 -0
- package/dist/ACP Connector/official-kernel/proxy.d.ts +14 -0
- package/dist/ACP Connector/official-kernel/proxy.js +197 -0
- package/dist/ACP Connector/official-kernel/proxy.js.map +1 -0
- package/dist/ACP Connector/official-kernel/run.d.ts +8 -0
- package/dist/ACP Connector/official-kernel/run.js +22 -0
- package/dist/ACP Connector/official-kernel/run.js.map +1 -0
- package/dist/ACP Connector/official-kernel/spawn.d.ts +5 -0
- package/dist/ACP Connector/official-kernel/spawn.js +33 -0
- package/dist/ACP Connector/official-kernel/spawn.js.map +1 -0
- package/dist/Admission Controller/controller.d.ts +338 -0
- package/dist/Admission Controller/controller.js +2089 -0
- package/dist/Admission Controller/controller.js.map +1 -0
- package/dist/Admission Controller/key-derivation.d.ts +24 -0
- package/dist/Admission Controller/key-derivation.js +141 -0
- package/dist/Admission Controller/key-derivation.js.map +1 -0
- package/dist/Admission Controller/key-store.d.ts +8 -0
- package/dist/Admission Controller/key-store.js +280 -0
- package/dist/Admission Controller/key-store.js.map +1 -0
- package/dist/Admission Controller/policy-limits.d.ts +6 -0
- package/dist/Admission Controller/policy-limits.js +11 -0
- package/dist/Admission Controller/policy-limits.js.map +1 -0
- package/dist/Admission Controller/process-evidence.d.ts +111 -0
- package/dist/Admission Controller/process-evidence.js +549 -0
- package/dist/Admission Controller/process-evidence.js.map +1 -0
- package/dist/Admission Controller/runtime-config.d.ts +23 -0
- package/dist/Admission Controller/runtime-config.js +86 -0
- package/dist/Admission Controller/runtime-config.js.map +1 -0
- package/dist/Admission Controller/schema.d.ts +12 -0
- package/dist/Admission Controller/schema.js +422 -0
- package/dist/Admission Controller/schema.js.map +1 -0
- package/package.json +72 -0
- package/scripts/prepare-admission-state-dir.mjs +62 -0
- package/scripts/prepare-official-kernel-compat.mjs +387 -0
|
@@ -0,0 +1,1904 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { chmodSync, closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { KERNEL_COMPAT_TARGET_NAMES, PRODUCTION_KERNEL_COMPAT_PINS } from "./kernel-compat-pins.js";
|
|
6
|
+
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
7
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8
|
+
const INTERNAL_DIRECTORY_PATTERN = /^[.A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
9
|
+
const ENVIRONMENT_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
10
|
+
const INTEGER_PATTERN = /^\d+$/;
|
|
11
|
+
const MAX_RECEIPT_BYTES = 64 * 1024;
|
|
12
|
+
const MAX_ACTIVATION_STATE_BYTES = 16 * 1024;
|
|
13
|
+
const MAX_LOCK_BYTES = 4 * 1024;
|
|
14
|
+
const MAX_PATCHED_SOURCE_BYTES = 16 * 1024 * 1024;
|
|
15
|
+
const MAX_PATCH_EDIT_COUNT = 16;
|
|
16
|
+
const MAX_PATCH_TEXT_BYTES = 64 * 1024;
|
|
17
|
+
const MAX_SYMLINK_CHAIN = 16;
|
|
18
|
+
const LOCK_INCOMPLETE_GRACE_MS = 30_000;
|
|
19
|
+
const RECEIPT_SCHEMA_VERSION = 2;
|
|
20
|
+
const ACTIVATION_STATE_SCHEMA_VERSION = 1;
|
|
21
|
+
const LOCK_SCHEMA_VERSION = 1;
|
|
22
|
+
const RELEASES_DIRECTORY = "releases";
|
|
23
|
+
const STAGING_DIRECTORY = ".staging";
|
|
24
|
+
const ACTIVATION_STATES_DIRECTORY = "activation-states";
|
|
25
|
+
const ACTIVE_REFERENCE = "active";
|
|
26
|
+
const LOCK_FILE = ".kernel-compat.lock";
|
|
27
|
+
const KERNEL_EXECUTABLE_FILE = "agy_acp_server";
|
|
28
|
+
const RUNFILES_DIRECTORY = "agy_acp_server.runfiles";
|
|
29
|
+
const EXTERNAL_HARNESS_FILE = "localharness_external";
|
|
30
|
+
const RELEASE_WRAPPER_FILE = "agy-acp-kernel-compat";
|
|
31
|
+
const RELEASE_VERIFIER_FILE = "kernel-compat-wrapper-verify.mjs";
|
|
32
|
+
const STABLE_WRAPPER_FILE = "agy-acp-kernel-compat-active";
|
|
33
|
+
const ACTIVE_RESOLVER_FILE = "kernel-compat-active-resolve.mjs";
|
|
34
|
+
const STALE_LOCK_QUARANTINE_PATTERN = /^\.kernel-compat\.lock\.stale-[a-f0-9]{16}$/;
|
|
35
|
+
const MANAGED_ACTIVATION_STATE_PATTERN = /^activation-[a-f0-9]{24}$/;
|
|
36
|
+
const REQUIRED_NOFOLLOW = constants.O_NOFOLLOW;
|
|
37
|
+
const CLOEXEC = 0;
|
|
38
|
+
const DIRECTORY = constants.O_DIRECTORY ?? 0;
|
|
39
|
+
export const DEFAULT_KERNEL_COMPAT_LAYOUT = Object.freeze({
|
|
40
|
+
kernelExecutableRelativePath: KERNEL_EXECUTABLE_FILE,
|
|
41
|
+
externalHarnessRelativePath: EXTERNAL_HARNESS_FILE,
|
|
42
|
+
compatModuleRelativePath: "paseo_model_compat.py",
|
|
43
|
+
importerEnvironment: Object.freeze({})
|
|
44
|
+
});
|
|
45
|
+
export class KernelCompatLifecycleError extends Error {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "KernelCompatLifecycleError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class OfficialKernelCompatLifecycle {
|
|
52
|
+
#stateRoot;
|
|
53
|
+
#pins;
|
|
54
|
+
#pinDigest;
|
|
55
|
+
#clock;
|
|
56
|
+
#hooks;
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.#stateRoot = requireAbsolutePath("state root", options.stateRoot);
|
|
59
|
+
this.#pins = snapshotPins(options.pins ?? PRODUCTION_KERNEL_COMPAT_PINS);
|
|
60
|
+
this.#pinDigest = sha256Text(canonicalJson(this.#pins));
|
|
61
|
+
this.#clock = options.clock ?? (() => new Date());
|
|
62
|
+
this.#hooks = options.hooks ?? {};
|
|
63
|
+
}
|
|
64
|
+
async prepare(options) {
|
|
65
|
+
const uid = requireNonRootLinuxUid();
|
|
66
|
+
ensureSecureDirectory(this.#stateRoot, "kernel compatibility root", uid, true);
|
|
67
|
+
return withWriterLock(this.#stateRoot, uid, this.#clock, () => this.prepareUnlocked(options, uid));
|
|
68
|
+
}
|
|
69
|
+
async verify(artifactId) {
|
|
70
|
+
const uid = requireNonRootLinuxUid();
|
|
71
|
+
ensureSecureDirectory(this.#stateRoot, "kernel compatibility root", uid, false);
|
|
72
|
+
const selectedArtifactId = artifactId === undefined
|
|
73
|
+
? requireActiveState(this.#stateRoot, uid, this.#pinDigest).currentArtifactId
|
|
74
|
+
: requireArtifactId(artifactId);
|
|
75
|
+
const releasePath = releasePathFor(this.#stateRoot, selectedArtifactId, uid);
|
|
76
|
+
const verified = verifyRelease(releasePath, this.#pins, this.#pinDigest, uid);
|
|
77
|
+
return {
|
|
78
|
+
artifactId: verified.receipt.artifactId,
|
|
79
|
+
releasePath,
|
|
80
|
+
wrapperPath: verified.wrapperPath,
|
|
81
|
+
verifiedAt: this.#clock().toISOString()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async activate(artifactId) {
|
|
85
|
+
const uid = requireNonRootLinuxUid();
|
|
86
|
+
ensureSecureDirectory(this.#stateRoot, "kernel compatibility root", uid, false);
|
|
87
|
+
const selectedArtifactId = requireArtifactId(artifactId);
|
|
88
|
+
return withWriterLock(this.#stateRoot, uid, this.#clock, () => {
|
|
89
|
+
const selected = verifyRelease(releasePathFor(this.#stateRoot, selectedArtifactId, uid), this.#pins, this.#pinDigest, uid);
|
|
90
|
+
ensureStableActivationWrapper(this.#stateRoot, this.#pinDigest, uid);
|
|
91
|
+
const active = readActiveState(this.#stateRoot, uid, this.#pinDigest);
|
|
92
|
+
if (active?.currentArtifactId === selectedArtifactId) {
|
|
93
|
+
return {
|
|
94
|
+
changed: false,
|
|
95
|
+
currentArtifactId: selectedArtifactId,
|
|
96
|
+
previousArtifactId: active.previousArtifactId,
|
|
97
|
+
wrapperPath: stableWrapperPathFor(this.#stateRoot),
|
|
98
|
+
releaseWrapperPath: selected.wrapperPath
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (active !== undefined) {
|
|
102
|
+
verifyRelease(releasePathFor(this.#stateRoot, active.currentArtifactId, uid), this.#pins, this.#pinDigest, uid);
|
|
103
|
+
}
|
|
104
|
+
const state = {
|
|
105
|
+
currentArtifactId: selectedArtifactId,
|
|
106
|
+
...(active === undefined ? {} : { previousArtifactId: active.currentArtifactId })
|
|
107
|
+
};
|
|
108
|
+
commitActivationState(this.#stateRoot, state, this.#pinDigest, uid, this.#hooks);
|
|
109
|
+
return {
|
|
110
|
+
changed: true,
|
|
111
|
+
...state,
|
|
112
|
+
wrapperPath: stableWrapperPathFor(this.#stateRoot),
|
|
113
|
+
releaseWrapperPath: selected.wrapperPath
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async rollback() {
|
|
118
|
+
const uid = requireNonRootLinuxUid();
|
|
119
|
+
ensureSecureDirectory(this.#stateRoot, "kernel compatibility root", uid, false);
|
|
120
|
+
return withWriterLock(this.#stateRoot, uid, this.#clock, () => {
|
|
121
|
+
const active = requireActiveState(this.#stateRoot, uid, this.#pinDigest);
|
|
122
|
+
if (active.previousArtifactId === undefined) {
|
|
123
|
+
throw new KernelCompatLifecycleError("no previous compatibility release is available for rollback");
|
|
124
|
+
}
|
|
125
|
+
const current = verifyRelease(releasePathFor(this.#stateRoot, active.currentArtifactId, uid), this.#pins, this.#pinDigest, uid);
|
|
126
|
+
const previous = verifyRelease(releasePathFor(this.#stateRoot, active.previousArtifactId, uid), this.#pins, this.#pinDigest, uid);
|
|
127
|
+
const state = {
|
|
128
|
+
currentArtifactId: active.previousArtifactId,
|
|
129
|
+
previousArtifactId: active.currentArtifactId
|
|
130
|
+
};
|
|
131
|
+
commitActivationState(this.#stateRoot, state, this.#pinDigest, uid, this.#hooks);
|
|
132
|
+
return {
|
|
133
|
+
changed: true,
|
|
134
|
+
...state,
|
|
135
|
+
wrapperPath: stableWrapperPathFor(this.#stateRoot),
|
|
136
|
+
releaseWrapperPath: previous.wrapperPath
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async status() {
|
|
141
|
+
const uid = requireNonRootLinuxUid();
|
|
142
|
+
const rootStatus = inspectSecureDirectory(this.#stateRoot, uid, false);
|
|
143
|
+
if (!rootStatus.exists) {
|
|
144
|
+
return {
|
|
145
|
+
stateRoot: this.#stateRoot,
|
|
146
|
+
rootExists: false,
|
|
147
|
+
rootSecure: false,
|
|
148
|
+
stagingEntries: [],
|
|
149
|
+
issues: []
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (rootStatus.error !== undefined) {
|
|
153
|
+
return {
|
|
154
|
+
stateRoot: this.#stateRoot,
|
|
155
|
+
rootExists: true,
|
|
156
|
+
rootSecure: false,
|
|
157
|
+
stagingEntries: [],
|
|
158
|
+
issues: [rootStatus.error]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const issues = [];
|
|
162
|
+
let active;
|
|
163
|
+
try {
|
|
164
|
+
active = readActiveState(this.#stateRoot, uid, this.#pinDigest);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
issues.push(`active: ${messageFor(error)}`);
|
|
168
|
+
}
|
|
169
|
+
const describe = (artifactId, label) => {
|
|
170
|
+
if (artifactId === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
try {
|
|
173
|
+
const releasePath = releasePathFor(this.#stateRoot, artifactId, uid);
|
|
174
|
+
const verified = verifyRelease(releasePath, this.#pins, this.#pinDigest, uid);
|
|
175
|
+
return { artifactId, wrapperPath: verified.wrapperPath, verified: true };
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const message = messageFor(error);
|
|
179
|
+
issues.push(`${label}: ${message}`);
|
|
180
|
+
return { artifactId, verified: false, error: message };
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const stableWrapperPath = stableWrapperPathFor(this.#stateRoot);
|
|
184
|
+
try {
|
|
185
|
+
assertOwnedRegularFile(stableWrapperPath, "stable activation wrapper", uid, true);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
issues.push(`stable wrapper: ${messageFor(error)}`);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
stateRoot: this.#stateRoot,
|
|
192
|
+
rootExists: true,
|
|
193
|
+
rootSecure: true,
|
|
194
|
+
stableWrapperPath,
|
|
195
|
+
current: describe(active?.currentArtifactId, "current"),
|
|
196
|
+
previous: describe(active?.previousArtifactId, "previous"),
|
|
197
|
+
stagingEntries: listSafeStagingEntries(this.#stateRoot, uid, issues),
|
|
198
|
+
issues
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async cleanup(options = {}) {
|
|
202
|
+
const uid = requireNonRootLinuxUid();
|
|
203
|
+
const rootStatus = inspectSecureDirectory(this.#stateRoot, uid, false);
|
|
204
|
+
if (!rootStatus.exists) {
|
|
205
|
+
return {
|
|
206
|
+
removedArtifactIds: [],
|
|
207
|
+
removedStagingEntries: [],
|
|
208
|
+
removedActivationStateDirectories: [],
|
|
209
|
+
removedStaleLockQuarantineFiles: [],
|
|
210
|
+
skippedEntries: []
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (rootStatus.error !== undefined)
|
|
214
|
+
throw new KernelCompatLifecycleError(rootStatus.error);
|
|
215
|
+
return withWriterLock(this.#stateRoot, uid, this.#clock, () => cleanupUnlocked(this.#stateRoot, uid, options));
|
|
216
|
+
}
|
|
217
|
+
prepareUnlocked(options, uid) {
|
|
218
|
+
const prepared = resolvePrepareOptions(options);
|
|
219
|
+
const patchDigest = digestPatchPlan(prepared.patchPlan);
|
|
220
|
+
// The only hot path that hashes the large official inputs is prepare().
|
|
221
|
+
// Their production distribution mode is 0775, so retain the verified FDs
|
|
222
|
+
// until their copies have independently been rechecked in private staging.
|
|
223
|
+
const parInput = openPinVerifiedInput(prepared.parPath, "official PAR", uid, true);
|
|
224
|
+
try {
|
|
225
|
+
const parDigest = parInput.digest;
|
|
226
|
+
if (parDigest.sha256 !== this.#pins.parSha256) {
|
|
227
|
+
throw new KernelCompatLifecycleError("official PAR hash does not match the pinned production artifact");
|
|
228
|
+
}
|
|
229
|
+
const externalHarnessInput = openPinVerifiedInput(prepared.externalHarnessPath, "external harness", uid, true);
|
|
230
|
+
try {
|
|
231
|
+
const externalHarnessDigest = externalHarnessInput.digest;
|
|
232
|
+
if (externalHarnessDigest.sha256 !== this.#pins.externalHarnessSha256) {
|
|
233
|
+
throw new KernelCompatLifecycleError("external harness hash does not match the pinned production artifact");
|
|
234
|
+
}
|
|
235
|
+
const compatDigest = digestRegularFile(prepared.compatModulePath, "compatibility module", uid, false);
|
|
236
|
+
const artifactId = buildArtifactId(this.#pins.profileId, this.#pins.parSha256, prepared.compatibilityVersion, compatDigest.sha256, patchDigest);
|
|
237
|
+
const releasesPath = ensureInternalDirectory(this.#stateRoot, RELEASES_DIRECTORY, uid);
|
|
238
|
+
const stagingPath = ensureInternalDirectory(this.#stateRoot, STAGING_DIRECTORY, uid);
|
|
239
|
+
if (statSync(releasesPath).dev !== statSync(stagingPath).dev) {
|
|
240
|
+
throw new KernelCompatLifecycleError("staging and releases must be on the same filesystem");
|
|
241
|
+
}
|
|
242
|
+
const finalReleasePath = path.join(releasesPath, artifactId);
|
|
243
|
+
const existing = tryLstat(finalReleasePath);
|
|
244
|
+
if (existing !== undefined) {
|
|
245
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
246
|
+
throw new KernelCompatLifecycleError("existing prepared release is not a real directory");
|
|
247
|
+
}
|
|
248
|
+
const verified = verifyRelease(finalReleasePath, this.#pins, this.#pinDigest, uid);
|
|
249
|
+
assertIdempotentReceipt(verified.receipt, parDigest, externalHarnessDigest, compatDigest, patchDigest);
|
|
250
|
+
ensureStableActivationWrapper(this.#stateRoot, this.#pinDigest, uid);
|
|
251
|
+
return {
|
|
252
|
+
artifactId,
|
|
253
|
+
releasePath: finalReleasePath,
|
|
254
|
+
wrapperPath: verified.wrapperPath,
|
|
255
|
+
stableWrapperPath: stableWrapperPathFor(this.#stateRoot),
|
|
256
|
+
receiptPath: path.join(finalReleasePath, "receipt.json"),
|
|
257
|
+
reused: true
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const stagingRoot = mkdtempSync(path.join(stagingPath, `kernel-compat-stage-${artifactId}-`), "utf8");
|
|
261
|
+
chmodSync(stagingRoot, 0o700);
|
|
262
|
+
try {
|
|
263
|
+
const stageReleasePath = path.join(stagingRoot, artifactId);
|
|
264
|
+
mkdirSync(stageReleasePath, { mode: 0o700 });
|
|
265
|
+
const stagedBinaryPath = path.join(stageReleasePath, KERNEL_EXECUTABLE_FILE);
|
|
266
|
+
const stagedHarnessPath = path.join(stageReleasePath, EXTERNAL_HARNESS_FILE);
|
|
267
|
+
// The real self-unpacker derives its adjacent .runfiles directory from argv[0].
|
|
268
|
+
copyPinVerifiedInput(parInput, stagedBinaryPath, "official PAR", uid, 0o500);
|
|
269
|
+
copyPinVerifiedInput(externalHarnessInput, stagedHarnessPath, "external harness", uid, 0o500);
|
|
270
|
+
runOfficialSelfUnpacker(stagedBinaryPath, stageReleasePath, stagingRoot);
|
|
271
|
+
assertAndHardenUnpackedKernelExecutable(stageReleasePath, stagedBinaryPath, uid);
|
|
272
|
+
const runfilesPath = path.join(stageReleasePath, RUNFILES_DIRECTORY);
|
|
273
|
+
assertRealDirectory(runfilesPath, "unpacked runfiles", uid);
|
|
274
|
+
hardenRunfiles(runfilesPath, stageReleasePath, mainSymlinkRelativePath(this.#pins), uid);
|
|
275
|
+
// The private stage is reopened only long enough to place the caller module.
|
|
276
|
+
chmodSync(runfilesPath, 0o700);
|
|
277
|
+
const preimagePaths = targetPaths(runfilesPath, this.#pins, uid);
|
|
278
|
+
for (const target of KERNEL_COMPAT_TARGET_NAMES) {
|
|
279
|
+
const digest = digestRegularFile(preimagePaths[target], `${target} preimage`, uid, false);
|
|
280
|
+
if (digest.sha256 !== this.#pins.targets[target].preimageSha256) {
|
|
281
|
+
throw new KernelCompatLifecycleError(`${target} preimage hash does not match the pinned production source`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
ensureParentDirectories(runfilesPath, prepared.layout.compatModuleRelativePath, uid);
|
|
285
|
+
const compatDestination = safePathUnderDirectory(runfilesPath, prepared.layout.compatModuleRelativePath, "compatibility module destination", false);
|
|
286
|
+
copyRegularFile(prepared.compatModulePath, compatDestination, "compatibility module", uid, compatDigest, 0o400);
|
|
287
|
+
applyPatchPlan(preimagePaths, prepared.patchPlan, uid);
|
|
288
|
+
const postimages = collectPostimages(runfilesPath, this.#pins, prepared.layout, uid);
|
|
289
|
+
if (postimages.serverControl.sha256 !== this.#pins.targets.serverControl.preimageSha256) {
|
|
290
|
+
throw new KernelCompatLifecycleError("server.py changed during preparation; this control source must never be modified");
|
|
291
|
+
}
|
|
292
|
+
hardenRunfiles(runfilesPath, stageReleasePath, mainSymlinkRelativePath(this.#pins), uid);
|
|
293
|
+
assertOwnedRegularFile(stagedBinaryPath, "staged kernel executable", uid, true, 0o500);
|
|
294
|
+
assertOwnedRegularFile(stagedHarnessPath, "staged external harness", uid, true, 0o500);
|
|
295
|
+
validateImporterEnvironment(runfilesPath, prepared.layout, uid);
|
|
296
|
+
const wrapperVerifierPath = path.join(stageReleasePath, RELEASE_VERIFIER_FILE);
|
|
297
|
+
writeNewFile(wrapperVerifierPath, renderReleaseWrapperVerifier(this.#pinDigest), 0o500);
|
|
298
|
+
const wrapperPath = path.join(stageReleasePath, RELEASE_WRAPPER_FILE);
|
|
299
|
+
writeNewFile(wrapperPath, renderReleaseWrapper(prepared.layout), 0o500);
|
|
300
|
+
const wrapperDigest = digestSmallFile(wrapperPath, "generated wrapper", uid, true);
|
|
301
|
+
const wrapperVerifierDigest = digestSmallFile(wrapperVerifierPath, "generated wrapper verifier", uid, true);
|
|
302
|
+
const receipt = {
|
|
303
|
+
schemaVersion: RECEIPT_SCHEMA_VERSION,
|
|
304
|
+
artifactId,
|
|
305
|
+
profileId: this.#pins.profileId,
|
|
306
|
+
createdAt: this.#clock().toISOString(),
|
|
307
|
+
pinDigest: this.#pinDigest,
|
|
308
|
+
patchDigest,
|
|
309
|
+
inputs: {
|
|
310
|
+
par: parDigest,
|
|
311
|
+
externalHarness: externalHarnessDigest,
|
|
312
|
+
compatModule: compatDigest
|
|
313
|
+
},
|
|
314
|
+
layout: {
|
|
315
|
+
runfilesRelativePath: RUNFILES_DIRECTORY,
|
|
316
|
+
kernelExecutableRelativePath: KERNEL_EXECUTABLE_FILE,
|
|
317
|
+
externalHarnessRelativePath: EXTERNAL_HARNESS_FILE,
|
|
318
|
+
compatModuleRelativePath: prepared.layout.compatModuleRelativePath,
|
|
319
|
+
importerEnvironment: prepared.layout.importerEnvironment
|
|
320
|
+
},
|
|
321
|
+
postimages,
|
|
322
|
+
wrapper: {
|
|
323
|
+
relativePath: RELEASE_WRAPPER_FILE,
|
|
324
|
+
sha256: wrapperDigest.sha256,
|
|
325
|
+
bytes: wrapperDigest.bytes,
|
|
326
|
+
verifierRelativePath: RELEASE_VERIFIER_FILE,
|
|
327
|
+
verifierSha256: wrapperVerifierDigest.sha256,
|
|
328
|
+
verifierBytes: wrapperVerifierDigest.bytes
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
writeNewFile(path.join(stageReleasePath, "receipt.json"), `${canonicalJson(receipt)}\n`, 0o600);
|
|
332
|
+
chmodSync(stageReleasePath, 0o500);
|
|
333
|
+
fsyncDirectory(runfilesPath);
|
|
334
|
+
fsyncDirectory(stageReleasePath);
|
|
335
|
+
fsyncDirectory(stagingRoot);
|
|
336
|
+
verifyRelease(stageReleasePath, this.#pins, this.#pinDigest, uid);
|
|
337
|
+
// This filesystem requires write permission on the staged directory itself for rename.
|
|
338
|
+
// Its parent remains private until the atomic publish below.
|
|
339
|
+
chmodSync(stageReleasePath, 0o700);
|
|
340
|
+
renameSync(stageReleasePath, finalReleasePath);
|
|
341
|
+
chmodSync(finalReleasePath, 0o500);
|
|
342
|
+
fsyncDirectory(finalReleasePath);
|
|
343
|
+
fsyncDirectory(releasesPath);
|
|
344
|
+
ensureStableActivationWrapper(this.#stateRoot, this.#pinDigest, uid);
|
|
345
|
+
return {
|
|
346
|
+
artifactId,
|
|
347
|
+
releasePath: finalReleasePath,
|
|
348
|
+
wrapperPath: path.join(finalReleasePath, RELEASE_WRAPPER_FILE),
|
|
349
|
+
stableWrapperPath: stableWrapperPathFor(this.#stateRoot),
|
|
350
|
+
receiptPath: path.join(finalReleasePath, "receipt.json"),
|
|
351
|
+
reused: false
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
removeDirectoryIfSafe(stagingPath, stagingRoot, "kernel-compat-stage-", uid);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
closeSync(externalHarnessInput.fd);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
closeSync(parInput.fd);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function resolvePrepareOptions(options) {
|
|
368
|
+
const compatibilityVersion = options.compatibilityVersion ?? "compat-v1";
|
|
369
|
+
requireIdentifier("compatibility version", compatibilityVersion);
|
|
370
|
+
const layout = resolveLayout(options.layout);
|
|
371
|
+
const patchPlan = validatePatchPlan(options.patchPlan);
|
|
372
|
+
return {
|
|
373
|
+
parPath: requireAbsolutePath("official PAR path", options.parPath),
|
|
374
|
+
externalHarnessPath: requireAbsolutePath("external harness path", options.externalHarnessPath),
|
|
375
|
+
compatModulePath: requireAbsolutePath("compatibility module path", options.compatModulePath),
|
|
376
|
+
patchPlan,
|
|
377
|
+
compatibilityVersion,
|
|
378
|
+
layout
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function resolveLayout(partial) {
|
|
382
|
+
const layout = {
|
|
383
|
+
kernelExecutableRelativePath: partial?.kernelExecutableRelativePath ?? KERNEL_EXECUTABLE_FILE,
|
|
384
|
+
externalHarnessRelativePath: partial?.externalHarnessRelativePath ?? EXTERNAL_HARNESS_FILE,
|
|
385
|
+
compatModuleRelativePath: partial?.compatModuleRelativePath ?? DEFAULT_KERNEL_COMPAT_LAYOUT.compatModuleRelativePath,
|
|
386
|
+
importerEnvironment: partial?.importerEnvironment ?? DEFAULT_KERNEL_COMPAT_LAYOUT.importerEnvironment
|
|
387
|
+
};
|
|
388
|
+
if (layout.kernelExecutableRelativePath !== KERNEL_EXECUTABLE_FILE) {
|
|
389
|
+
throw new KernelCompatLifecycleError("kernel executable must be the release-root agy_acp_server copy");
|
|
390
|
+
}
|
|
391
|
+
if (layout.externalHarnessRelativePath !== EXTERNAL_HARNESS_FILE) {
|
|
392
|
+
throw new KernelCompatLifecycleError("external harness must be the release-root localharness_external copy");
|
|
393
|
+
}
|
|
394
|
+
assertSafeRelativePath("compatibility module destination", layout.compatModuleRelativePath);
|
|
395
|
+
for (const [name, relativePath] of Object.entries(layout.importerEnvironment)) {
|
|
396
|
+
if (!ENVIRONMENT_NAME_PATTERN.test(name)) {
|
|
397
|
+
throw new KernelCompatLifecycleError("importer environment names must be uppercase shell identifiers");
|
|
398
|
+
}
|
|
399
|
+
if (["ANTIGRAVITY_HARNESS_PATH", "PYTHONDONTWRITEBYTECODE", "PYTHONPATH"].includes(name)) {
|
|
400
|
+
throw new KernelCompatLifecycleError(`${name} is controlled by the generated wrapper`);
|
|
401
|
+
}
|
|
402
|
+
assertSafeRelativePath(`importer environment ${name}`, relativePath);
|
|
403
|
+
}
|
|
404
|
+
return Object.freeze({
|
|
405
|
+
...layout,
|
|
406
|
+
importerEnvironment: Object.freeze({ ...layout.importerEnvironment })
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function validatePatchPlan(plan) {
|
|
410
|
+
if (plan === null || typeof plan !== "object" || !Array.isArray(plan.edits)) {
|
|
411
|
+
throw new KernelCompatLifecycleError("patch plan must contain an edits array");
|
|
412
|
+
}
|
|
413
|
+
if (plan.edits.length === 0 || plan.edits.length > MAX_PATCH_EDIT_COUNT) {
|
|
414
|
+
throw new KernelCompatLifecycleError(`patch plan edit count exceeds the limit of ${MAX_PATCH_EDIT_COUNT}`);
|
|
415
|
+
}
|
|
416
|
+
const ids = new Set();
|
|
417
|
+
const targetCounts = { modelSelection: 0, proxyServer: 0 };
|
|
418
|
+
const edits = plan.edits.map((edit) => {
|
|
419
|
+
if (edit === null || typeof edit !== "object") {
|
|
420
|
+
throw new KernelCompatLifecycleError("patch plan contains an invalid edit");
|
|
421
|
+
}
|
|
422
|
+
requireIdentifier("patch marker id", edit.id);
|
|
423
|
+
if (ids.has(edit.id))
|
|
424
|
+
throw new KernelCompatLifecycleError(`patch marker id ${edit.id} is duplicated`);
|
|
425
|
+
ids.add(edit.id);
|
|
426
|
+
const target = edit.target;
|
|
427
|
+
if (target !== "modelSelection" && target !== "proxyServer") {
|
|
428
|
+
throw new KernelCompatLifecycleError("patch edits may target only modelSelection or proxyServer");
|
|
429
|
+
}
|
|
430
|
+
assertPatchText(edit.find, `patch marker ${edit.id} exact match`);
|
|
431
|
+
assertPatchText(edit.replacement, `patch marker ${edit.id} replacement`);
|
|
432
|
+
if (edit.find === edit.replacement) {
|
|
433
|
+
throw new KernelCompatLifecycleError(`patch marker ${edit.id} replacement must change the matched text`);
|
|
434
|
+
}
|
|
435
|
+
if (target === "modelSelection")
|
|
436
|
+
targetCounts.modelSelection += 1;
|
|
437
|
+
else
|
|
438
|
+
targetCounts.proxyServer += 1;
|
|
439
|
+
return Object.freeze({ ...edit, target });
|
|
440
|
+
});
|
|
441
|
+
if (targetCounts.modelSelection === 0 || targetCounts.proxyServer === 0) {
|
|
442
|
+
throw new KernelCompatLifecycleError("patch plan must contain exact edits for modelSelection and proxyServer");
|
|
443
|
+
}
|
|
444
|
+
return Object.freeze({ edits: Object.freeze(edits) });
|
|
445
|
+
}
|
|
446
|
+
function assertPatchText(value, label) {
|
|
447
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
448
|
+
throw new KernelCompatLifecycleError(`${label} must be a non-empty string`);
|
|
449
|
+
}
|
|
450
|
+
if (Buffer.byteLength(value, "utf8") > MAX_PATCH_TEXT_BYTES) {
|
|
451
|
+
throw new KernelCompatLifecycleError(`${label} exceeds the ${MAX_PATCH_TEXT_BYTES}-byte limit`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function validatePins(pins) {
|
|
455
|
+
if (pins === null || typeof pins !== "object")
|
|
456
|
+
throw new KernelCompatLifecycleError("kernel compatibility pins are invalid");
|
|
457
|
+
requireIdentifier("pin profile", pins.profileId);
|
|
458
|
+
requireSha256("PAR pin", pins.parSha256);
|
|
459
|
+
requireSha256("external harness pin", pins.externalHarnessSha256);
|
|
460
|
+
for (const target of KERNEL_COMPAT_TARGET_NAMES) {
|
|
461
|
+
const pin = pins.targets?.[target];
|
|
462
|
+
if (pin === undefined || typeof pin !== "object")
|
|
463
|
+
throw new KernelCompatLifecycleError(`missing ${target} pin`);
|
|
464
|
+
assertSafeRelativePath(`${target} pin path`, pin.relativePath);
|
|
465
|
+
requireSha256(`${target} preimage pin`, pin.preimageSha256);
|
|
466
|
+
if (target === "serverControl" && pin.patchable) {
|
|
467
|
+
throw new KernelCompatLifecycleError("serverControl pin must be non-patchable");
|
|
468
|
+
}
|
|
469
|
+
if (target !== "serverControl" && !pin.patchable) {
|
|
470
|
+
throw new KernelCompatLifecycleError(`${target} pin must be patchable`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function snapshotPins(pins) {
|
|
475
|
+
validatePins(pins);
|
|
476
|
+
return Object.freeze({
|
|
477
|
+
profileId: pins.profileId,
|
|
478
|
+
parSha256: pins.parSha256,
|
|
479
|
+
externalHarnessSha256: pins.externalHarnessSha256,
|
|
480
|
+
targets: Object.freeze({
|
|
481
|
+
modelSelection: Object.freeze({ ...pins.targets.modelSelection }),
|
|
482
|
+
proxyServer: Object.freeze({ ...pins.targets.proxyServer }),
|
|
483
|
+
serverControl: Object.freeze({ ...pins.targets.serverControl })
|
|
484
|
+
})
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function requireNonRootLinuxUid() {
|
|
488
|
+
if (process.platform !== "linux") {
|
|
489
|
+
throw new KernelCompatLifecycleError("kernel compatibility lifecycle is pinned to Linux process-identity semantics");
|
|
490
|
+
}
|
|
491
|
+
if (typeof process.getuid !== "function") {
|
|
492
|
+
throw new KernelCompatLifecycleError("kernel compatibility lifecycle requires a POSIX user identity");
|
|
493
|
+
}
|
|
494
|
+
const uid = process.getuid();
|
|
495
|
+
if (uid === 0)
|
|
496
|
+
throw new KernelCompatLifecycleError("kernel compatibility lifecycle refuses root mode");
|
|
497
|
+
return uid;
|
|
498
|
+
}
|
|
499
|
+
function requireAbsolutePath(label, value) {
|
|
500
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0") || !path.isAbsolute(value)) {
|
|
501
|
+
throw new KernelCompatLifecycleError(`${label} must be a non-empty absolute path`);
|
|
502
|
+
}
|
|
503
|
+
return path.resolve(value);
|
|
504
|
+
}
|
|
505
|
+
function requireIdentifier(label, value) {
|
|
506
|
+
if (typeof value !== "string" || !IDENTIFIER_PATTERN.test(value)) {
|
|
507
|
+
throw new KernelCompatLifecycleError(`${label} must contain only letters, digits, dots, underscores, or hyphens`);
|
|
508
|
+
}
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
function requireArtifactId(value) {
|
|
512
|
+
return requireIdentifier("artifact id", value);
|
|
513
|
+
}
|
|
514
|
+
function requireSha256(label, value) {
|
|
515
|
+
if (typeof value !== "string" || !HASH_PATTERN.test(value)) {
|
|
516
|
+
throw new KernelCompatLifecycleError(`${label} must be a lowercase SHA-256 digest`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function assertSafeRelativePath(label, value) {
|
|
520
|
+
if (typeof value !== "string" ||
|
|
521
|
+
value.length === 0 ||
|
|
522
|
+
value.includes("\0") ||
|
|
523
|
+
value.includes("\\") ||
|
|
524
|
+
path.isAbsolute(value) ||
|
|
525
|
+
path.posix.normalize(value) !== value ||
|
|
526
|
+
value.split("/").some((part) => part.length === 0 || part === "." || part === "..")) {
|
|
527
|
+
throw new KernelCompatLifecycleError(`${label} must be a normalized relative path`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function inspectSecureDirectory(directory, uid, create) {
|
|
531
|
+
try {
|
|
532
|
+
if (create) {
|
|
533
|
+
assertExistingPathHasNoSymlinkComponents(directory, "kernel compatibility root");
|
|
534
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
535
|
+
}
|
|
536
|
+
assertExistingPathHasNoSymlinkComponents(directory, "kernel compatibility root");
|
|
537
|
+
const stat = tryLstat(directory);
|
|
538
|
+
if (stat === undefined)
|
|
539
|
+
return { exists: false };
|
|
540
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
541
|
+
return { exists: true, error: "kernel compatibility root must be a real directory" };
|
|
542
|
+
if (stat.uid !== uid)
|
|
543
|
+
return { exists: true, error: "kernel compatibility root must be owned by the current user" };
|
|
544
|
+
if ((stat.mode & 0o777) !== 0o700)
|
|
545
|
+
return { exists: true, error: "kernel compatibility root must have mode 0700" };
|
|
546
|
+
return { exists: true };
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
return { exists: true, error: messageFor(error) };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function ensureSecureDirectory(directory, label, uid, create) {
|
|
553
|
+
const result = inspectSecureDirectory(directory, uid, create);
|
|
554
|
+
if (!result.exists)
|
|
555
|
+
throw new KernelCompatLifecycleError(`${label} does not exist`);
|
|
556
|
+
if (result.error !== undefined)
|
|
557
|
+
throw new KernelCompatLifecycleError(result.error.replace("kernel compatibility root", label));
|
|
558
|
+
}
|
|
559
|
+
function assertExistingPathHasNoSymlinkComponents(candidate, label) {
|
|
560
|
+
const resolved = path.resolve(candidate);
|
|
561
|
+
const parsed = path.parse(resolved);
|
|
562
|
+
let cursor = parsed.root;
|
|
563
|
+
for (const part of resolved.slice(parsed.root.length).split(path.sep).filter(Boolean)) {
|
|
564
|
+
cursor = path.join(cursor, part);
|
|
565
|
+
const stat = tryLstat(cursor);
|
|
566
|
+
if (stat === undefined)
|
|
567
|
+
return;
|
|
568
|
+
if (stat.isSymbolicLink())
|
|
569
|
+
throw new KernelCompatLifecycleError(`${label} must not traverse a symbolic link`);
|
|
570
|
+
if (!stat.isDirectory() && cursor !== resolved)
|
|
571
|
+
throw new KernelCompatLifecycleError(`${label} has a non-directory parent`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function ensureInternalDirectory(parent, name, uid) {
|
|
575
|
+
if (!INTERNAL_DIRECTORY_PATTERN.test(name) || name === "." || name === "..") {
|
|
576
|
+
throw new KernelCompatLifecycleError("internal directory must contain only letters, digits, dots, underscores, or hyphens");
|
|
577
|
+
}
|
|
578
|
+
const directory = path.join(parent, name);
|
|
579
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
580
|
+
assertRealDirectory(directory, `internal directory ${name}`, uid, 0o700);
|
|
581
|
+
return directory;
|
|
582
|
+
}
|
|
583
|
+
function assertRealDirectory(directory, label, uid, expectedMode) {
|
|
584
|
+
const stat = lstatSync(directory);
|
|
585
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
586
|
+
throw new KernelCompatLifecycleError(`${label} must be a real directory`);
|
|
587
|
+
if (stat.uid !== uid)
|
|
588
|
+
throw new KernelCompatLifecycleError(`${label} must be owned by the current user`);
|
|
589
|
+
if (expectedMode !== undefined && (stat.mode & 0o777) !== expectedMode) {
|
|
590
|
+
throw new KernelCompatLifecycleError(`${label} must have mode ${expectedMode.toString(8).padStart(4, "0")}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function requiredNoFollow() {
|
|
594
|
+
if (REQUIRED_NOFOLLOW === undefined) {
|
|
595
|
+
throw new KernelCompatLifecycleError("kernel compatibility lifecycle requires O_NOFOLLOW support");
|
|
596
|
+
}
|
|
597
|
+
return REQUIRED_NOFOLLOW;
|
|
598
|
+
}
|
|
599
|
+
function openOwnedRegularFile(filePath, label, uid, executable) {
|
|
600
|
+
return openCurrentUserRegularFile(filePath, label, uid, executable, false);
|
|
601
|
+
}
|
|
602
|
+
function openCurrentUserRegularFile(filePath, label, uid, executable, allowGroupWritable) {
|
|
603
|
+
assertExistingPathHasNoSymlinkComponents(filePath, label);
|
|
604
|
+
const lstat = lstatSync(filePath);
|
|
605
|
+
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
606
|
+
throw new KernelCompatLifecycleError(`${label} must be a regular file and not a symbolic link`);
|
|
607
|
+
}
|
|
608
|
+
if (lstat.uid !== uid)
|
|
609
|
+
throw new KernelCompatLifecycleError(`${label} must be owned by the current user`);
|
|
610
|
+
if (!allowGroupWritable && (lstat.mode & 0o022) !== 0) {
|
|
611
|
+
throw new KernelCompatLifecycleError(`${label} must not be group- or world-writable`);
|
|
612
|
+
}
|
|
613
|
+
if (allowGroupWritable && (lstat.mode & 0o002) !== 0) {
|
|
614
|
+
throw new KernelCompatLifecycleError(`${label} must not be world-writable`);
|
|
615
|
+
}
|
|
616
|
+
if (executable && (lstat.mode & 0o111) === 0)
|
|
617
|
+
throw new KernelCompatLifecycleError(`${label} must be executable`);
|
|
618
|
+
const fd = openSync(filePath, constants.O_RDONLY | requiredNoFollow() | CLOEXEC);
|
|
619
|
+
try {
|
|
620
|
+
const opened = fstatSync(fd);
|
|
621
|
+
if (!opened.isFile() || !sameFileSnapshot(opened, lstat) || opened.uid !== uid) {
|
|
622
|
+
throw new KernelCompatLifecycleError(`${label} changed while it was being opened`);
|
|
623
|
+
}
|
|
624
|
+
return { fd, before: opened };
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
closeSync(fd);
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function sameFileSnapshot(left, right) {
|
|
632
|
+
return left.dev === right.dev && left.ino === right.ino && left.uid === right.uid &&
|
|
633
|
+
left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
634
|
+
}
|
|
635
|
+
function assertStableFile(fd, before, label) {
|
|
636
|
+
const after = fstatSync(fd);
|
|
637
|
+
if (!sameFileSnapshot(after, before)) {
|
|
638
|
+
throw new KernelCompatLifecycleError(`${label} changed while it was being read`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function openPinVerifiedInput(filePath, label, uid, executable) {
|
|
642
|
+
const opened = openCurrentUserRegularFile(filePath, label, uid, executable, true);
|
|
643
|
+
try {
|
|
644
|
+
const digest = digestOpenFile(opened.fd);
|
|
645
|
+
assertStableFile(opened.fd, opened.before, label);
|
|
646
|
+
return { ...opened, digest };
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
closeSync(opened.fd);
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function digestRegularFile(filePath, label, uid, executable) {
|
|
654
|
+
const opened = openOwnedRegularFile(filePath, label, uid, executable);
|
|
655
|
+
try {
|
|
656
|
+
const digest = digestOpenFile(opened.fd);
|
|
657
|
+
assertStableFile(opened.fd, opened.before, label);
|
|
658
|
+
return digest;
|
|
659
|
+
}
|
|
660
|
+
finally {
|
|
661
|
+
closeSync(opened.fd);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function digestSmallFile(filePath, label, uid, executable) {
|
|
665
|
+
const opened = openOwnedRegularFile(filePath, label, uid, executable);
|
|
666
|
+
try {
|
|
667
|
+
if (opened.before.size > MAX_PATCHED_SOURCE_BYTES) {
|
|
668
|
+
throw new KernelCompatLifecycleError(`${label} exceeds the small-file verification limit`);
|
|
669
|
+
}
|
|
670
|
+
const digest = digestOpenFile(opened.fd);
|
|
671
|
+
assertStableFile(opened.fd, opened.before, label);
|
|
672
|
+
return digest;
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
closeSync(opened.fd);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function digestOpenFile(fd) {
|
|
679
|
+
const hash = createHash("sha256");
|
|
680
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
681
|
+
let bytes = 0;
|
|
682
|
+
let position = 0;
|
|
683
|
+
while (true) {
|
|
684
|
+
const count = readSync(fd, buffer, 0, buffer.length, position);
|
|
685
|
+
if (count === 0)
|
|
686
|
+
break;
|
|
687
|
+
hash.update(buffer.subarray(0, count));
|
|
688
|
+
bytes += count;
|
|
689
|
+
position += count;
|
|
690
|
+
}
|
|
691
|
+
return { sha256: hash.digest("hex"), bytes };
|
|
692
|
+
}
|
|
693
|
+
function copyPinVerifiedInput(source, destinationPath, label, uid, mode) {
|
|
694
|
+
let destination;
|
|
695
|
+
try {
|
|
696
|
+
destination = openSync(destinationPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | requiredNoFollow() | CLOEXEC, mode);
|
|
697
|
+
const hash = createHash("sha256");
|
|
698
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
699
|
+
let bytes = 0;
|
|
700
|
+
let position = 0;
|
|
701
|
+
while (true) {
|
|
702
|
+
const count = readSync(source.fd, buffer, 0, buffer.length, position);
|
|
703
|
+
if (count === 0)
|
|
704
|
+
break;
|
|
705
|
+
hash.update(buffer.subarray(0, count));
|
|
706
|
+
writeAll(destination, buffer.subarray(0, count));
|
|
707
|
+
bytes += count;
|
|
708
|
+
position += count;
|
|
709
|
+
}
|
|
710
|
+
fsyncSync(destination);
|
|
711
|
+
const copied = { sha256: hash.digest("hex"), bytes };
|
|
712
|
+
assertStableFile(source.fd, source.before, label);
|
|
713
|
+
if (copied.sha256 !== source.digest.sha256 || copied.bytes !== source.digest.bytes) {
|
|
714
|
+
throw new KernelCompatLifecycleError(`${label} changed before it could be staged`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
finally {
|
|
718
|
+
if (destination !== undefined)
|
|
719
|
+
closeSync(destination);
|
|
720
|
+
}
|
|
721
|
+
chmodSync(destinationPath, mode);
|
|
722
|
+
fsyncFile(destinationPath);
|
|
723
|
+
assertOwnedRegularFile(destinationPath, `staged ${label}`, uid, true, mode);
|
|
724
|
+
const copiedDigest = digestRegularFile(destinationPath, `staged ${label}`, uid, true);
|
|
725
|
+
if (copiedDigest.sha256 !== source.digest.sha256 || copiedDigest.bytes !== source.digest.bytes) {
|
|
726
|
+
throw new KernelCompatLifecycleError(`staged ${label} hash does not match the pin-verified input`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function copyRegularFile(sourcePath, destinationPath, label, uid, expected, mode) {
|
|
730
|
+
const source = openOwnedRegularFile(sourcePath, label, uid, (mode & 0o111) !== 0);
|
|
731
|
+
let destination;
|
|
732
|
+
try {
|
|
733
|
+
destination = openSync(destinationPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | requiredNoFollow() | CLOEXEC, mode);
|
|
734
|
+
const hash = createHash("sha256");
|
|
735
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
736
|
+
let bytes = 0;
|
|
737
|
+
while (true) {
|
|
738
|
+
const count = readSync(source.fd, buffer, 0, buffer.length, null);
|
|
739
|
+
if (count === 0)
|
|
740
|
+
break;
|
|
741
|
+
hash.update(buffer.subarray(0, count));
|
|
742
|
+
writeAll(destination, buffer.subarray(0, count));
|
|
743
|
+
bytes += count;
|
|
744
|
+
}
|
|
745
|
+
fsyncSync(destination);
|
|
746
|
+
const actual = { sha256: hash.digest("hex"), bytes };
|
|
747
|
+
assertStableFile(source.fd, source.before, label);
|
|
748
|
+
if (actual.sha256 !== expected.sha256 || actual.bytes !== expected.bytes) {
|
|
749
|
+
throw new KernelCompatLifecycleError(`${label} changed before it could be staged`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
finally {
|
|
753
|
+
if (destination !== undefined)
|
|
754
|
+
closeSync(destination);
|
|
755
|
+
closeSync(source.fd);
|
|
756
|
+
}
|
|
757
|
+
chmodSync(destinationPath, mode);
|
|
758
|
+
}
|
|
759
|
+
function writeAll(fd, data) {
|
|
760
|
+
let offset = 0;
|
|
761
|
+
while (offset < data.length) {
|
|
762
|
+
const written = writeSync(fd, data, offset, data.length - offset);
|
|
763
|
+
if (written <= 0)
|
|
764
|
+
throw new KernelCompatLifecycleError("could not write staged kernel compatibility data");
|
|
765
|
+
offset += written;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function writeNewFile(filePath, content, mode) {
|
|
769
|
+
const fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | requiredNoFollow() | CLOEXEC, mode);
|
|
770
|
+
try {
|
|
771
|
+
writeAll(fd, Buffer.from(content, "utf8"));
|
|
772
|
+
fsyncSync(fd);
|
|
773
|
+
}
|
|
774
|
+
finally {
|
|
775
|
+
closeSync(fd);
|
|
776
|
+
}
|
|
777
|
+
chmodSync(filePath, mode);
|
|
778
|
+
}
|
|
779
|
+
function readSmallTextFile(filePath, label, uid) {
|
|
780
|
+
const opened = openOwnedRegularFile(filePath, label, uid, false);
|
|
781
|
+
try {
|
|
782
|
+
if (opened.before.size > MAX_PATCHED_SOURCE_BYTES) {
|
|
783
|
+
throw new KernelCompatLifecycleError(`${label} exceeds the patched-source size limit`);
|
|
784
|
+
}
|
|
785
|
+
const content = readFileSync(opened.fd, "utf8");
|
|
786
|
+
assertStableFile(opened.fd, opened.before, label);
|
|
787
|
+
return content;
|
|
788
|
+
}
|
|
789
|
+
finally {
|
|
790
|
+
closeSync(opened.fd);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function rewriteSmallTextFile(filePath, content, label, uid) {
|
|
794
|
+
const lstat = lstatSync(filePath);
|
|
795
|
+
if (lstat.isSymbolicLink() || !lstat.isFile() || lstat.uid !== uid) {
|
|
796
|
+
throw new KernelCompatLifecycleError(`${label} is no longer a safe regular file`);
|
|
797
|
+
}
|
|
798
|
+
chmodSync(filePath, 0o600);
|
|
799
|
+
const fd = openSync(filePath, constants.O_WRONLY | constants.O_TRUNC | requiredNoFollow() | CLOEXEC);
|
|
800
|
+
try {
|
|
801
|
+
const opened = fstatSync(fd);
|
|
802
|
+
if (!opened.isFile() || opened.dev !== lstat.dev || opened.ino !== lstat.ino || opened.uid !== uid) {
|
|
803
|
+
throw new KernelCompatLifecycleError(`${label} changed while it was being patched`);
|
|
804
|
+
}
|
|
805
|
+
writeAll(fd, Buffer.from(content, "utf8"));
|
|
806
|
+
fsyncSync(fd);
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
closeSync(fd);
|
|
810
|
+
}
|
|
811
|
+
chmodSync(filePath, 0o400);
|
|
812
|
+
}
|
|
813
|
+
function runOfficialSelfUnpacker(stagedBinaryPath, releaseRoot, stagingRoot) {
|
|
814
|
+
const unpackHome = path.join(stagingRoot, "unpack-home");
|
|
815
|
+
mkdirSync(unpackHome, { mode: 0o700 });
|
|
816
|
+
const result = spawnSync(stagedBinaryPath, ["--unpack_par_and_exit"], {
|
|
817
|
+
cwd: releaseRoot,
|
|
818
|
+
encoding: "utf8",
|
|
819
|
+
stdio: "ignore",
|
|
820
|
+
timeout: 300_000,
|
|
821
|
+
env: {
|
|
822
|
+
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
823
|
+
HOME: unpackHome,
|
|
824
|
+
XDG_CACHE_HOME: path.join(unpackHome, ".cache"),
|
|
825
|
+
XDG_CONFIG_HOME: path.join(unpackHome, ".config"),
|
|
826
|
+
LANG: "C",
|
|
827
|
+
LC_ALL: "C",
|
|
828
|
+
http_proxy: "",
|
|
829
|
+
https_proxy: "",
|
|
830
|
+
HTTP_PROXY: "",
|
|
831
|
+
HTTPS_PROXY: "",
|
|
832
|
+
ALL_PROXY: "",
|
|
833
|
+
all_proxy: ""
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
if (result.error !== undefined || result.status !== 0 || result.signal !== null) {
|
|
837
|
+
throw new KernelCompatLifecycleError("official self-unpacker did not complete successfully");
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function assertAndHardenUnpackedKernelExecutable(releaseRoot, stagedBinaryPath, uid) {
|
|
841
|
+
assertRealDirectory(releaseRoot, "unpacked release root", uid, 0o700);
|
|
842
|
+
const executablePath = safePathUnderDirectory(releaseRoot, KERNEL_EXECUTABLE_FILE, "unpacked adjacent kernel executable", true);
|
|
843
|
+
if (path.resolve(executablePath) !== path.resolve(stagedBinaryPath)) {
|
|
844
|
+
throw new KernelCompatLifecycleError("unpacked adjacent kernel executable is outside the private stage");
|
|
845
|
+
}
|
|
846
|
+
const opened = openCurrentUserRegularFile(executablePath, "unpacked adjacent kernel executable", uid, true, true);
|
|
847
|
+
try {
|
|
848
|
+
fchmodSync(opened.fd, 0o500);
|
|
849
|
+
fsyncSync(opened.fd);
|
|
850
|
+
const hardened = fstatSync(opened.fd);
|
|
851
|
+
if (!hardened.isFile() || hardened.uid !== uid || (hardened.mode & 0o777) !== 0o500) {
|
|
852
|
+
throw new KernelCompatLifecycleError("unpacked adjacent kernel executable could not be hardened to mode 0500");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
finally {
|
|
856
|
+
closeSync(opened.fd);
|
|
857
|
+
}
|
|
858
|
+
assertOwnedRegularFile(executablePath, "unpacked adjacent kernel executable", uid, true, 0o500);
|
|
859
|
+
}
|
|
860
|
+
function targetPaths(runfilesPath, pins, uid) {
|
|
861
|
+
const paths = {};
|
|
862
|
+
for (const target of KERNEL_COMPAT_TARGET_NAMES) {
|
|
863
|
+
const targetPath = safePathUnderDirectory(runfilesPath, pins.targets[target].relativePath, `${target} source`, true);
|
|
864
|
+
assertOwnedRegularFile(targetPath, `${target} source`, uid, false);
|
|
865
|
+
paths[target] = targetPath;
|
|
866
|
+
}
|
|
867
|
+
return paths;
|
|
868
|
+
}
|
|
869
|
+
function applyPatchPlan(paths, patchPlan, uid) {
|
|
870
|
+
for (const target of ["modelSelection", "proxyServer"]) {
|
|
871
|
+
const source = readSmallTextFile(paths[target], `${target} source`, uid);
|
|
872
|
+
const edits = patchPlan.edits.filter((edit) => edit.target === target);
|
|
873
|
+
let patched = source;
|
|
874
|
+
for (const edit of edits) {
|
|
875
|
+
const occurrences = countExactOccurrences(patched, edit.find);
|
|
876
|
+
if (occurrences !== 1) {
|
|
877
|
+
throw new KernelCompatLifecycleError(`patch marker ${edit.id} in ${target} must occur exactly once; found ${occurrences}`);
|
|
878
|
+
}
|
|
879
|
+
patched = patched.replace(edit.find, edit.replacement);
|
|
880
|
+
}
|
|
881
|
+
rewriteSmallTextFile(paths[target], patched, `${target} source`, uid);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function countExactOccurrences(source, marker) {
|
|
885
|
+
let count = 0;
|
|
886
|
+
let start = 0;
|
|
887
|
+
while (true) {
|
|
888
|
+
const index = source.indexOf(marker, start);
|
|
889
|
+
if (index === -1)
|
|
890
|
+
return count;
|
|
891
|
+
count += 1;
|
|
892
|
+
start = index + marker.length;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function collectPostimages(runfilesPath, pins, layout, uid) {
|
|
896
|
+
const postimages = {};
|
|
897
|
+
for (const target of KERNEL_COMPAT_TARGET_NAMES) {
|
|
898
|
+
const relativePath = pins.targets[target].relativePath;
|
|
899
|
+
const digest = digestSmallFile(safePathUnderDirectory(runfilesPath, relativePath, `${target} postimage`, true), `${target} postimage`, uid, false);
|
|
900
|
+
postimages[target] = { relativePath, ...digest };
|
|
901
|
+
}
|
|
902
|
+
const compatDigest = digestSmallFile(safePathUnderDirectory(runfilesPath, layout.compatModuleRelativePath, "compatibility module postimage", true), "compatibility module postimage", uid, false);
|
|
903
|
+
postimages.compatModule = { relativePath: layout.compatModuleRelativePath, ...compatDigest };
|
|
904
|
+
return postimages;
|
|
905
|
+
}
|
|
906
|
+
function validateImporterEnvironment(runfilesPath, layout, uid) {
|
|
907
|
+
for (const [name, relativePath] of Object.entries(layout.importerEnvironment)) {
|
|
908
|
+
const importerPath = safePathUnderDirectory(runfilesPath, relativePath, `importer environment ${name}`, true);
|
|
909
|
+
const stat = lstatSync(importerPath);
|
|
910
|
+
if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile()) || stat.uid !== uid) {
|
|
911
|
+
throw new KernelCompatLifecycleError(`importer environment ${name} must point to a current-user-owned real path`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function assertOwnedRegularFile(filePath, label, uid, executable, exactMode) {
|
|
916
|
+
const stat = lstatSync(filePath);
|
|
917
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.uid !== uid) {
|
|
918
|
+
throw new KernelCompatLifecycleError(`${label} must be a current-user-owned regular file`);
|
|
919
|
+
}
|
|
920
|
+
if ((stat.mode & 0o022) !== 0)
|
|
921
|
+
throw new KernelCompatLifecycleError(`${label} must not be group- or world-writable`);
|
|
922
|
+
if (executable && (stat.mode & 0o111) === 0)
|
|
923
|
+
throw new KernelCompatLifecycleError(`${label} must be executable`);
|
|
924
|
+
if (exactMode !== undefined && (stat.mode & 0o777) !== exactMode) {
|
|
925
|
+
throw new KernelCompatLifecycleError(`${label} must have mode ${exactMode.toString(8).padStart(4, "0")}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function safePathUnderDirectory(root, relativePath, label, requireExisting) {
|
|
929
|
+
assertSafeRelativePath(label, relativePath);
|
|
930
|
+
const resolvedRoot = path.resolve(root);
|
|
931
|
+
const candidate = path.resolve(resolvedRoot, relativePath);
|
|
932
|
+
if (!isInsideDirectory(candidate, resolvedRoot)) {
|
|
933
|
+
throw new KernelCompatLifecycleError(`${label} escapes its expected directory`);
|
|
934
|
+
}
|
|
935
|
+
let cursor = resolvedRoot;
|
|
936
|
+
const parts = relativePath.split("/");
|
|
937
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
938
|
+
cursor = path.join(cursor, parts[index]);
|
|
939
|
+
const stat = tryLstat(cursor);
|
|
940
|
+
const finalPart = index === parts.length - 1;
|
|
941
|
+
if (stat === undefined) {
|
|
942
|
+
if (finalPart && !requireExisting)
|
|
943
|
+
return candidate;
|
|
944
|
+
throw new KernelCompatLifecycleError(`${label} is missing`);
|
|
945
|
+
}
|
|
946
|
+
if (stat.isSymbolicLink())
|
|
947
|
+
throw new KernelCompatLifecycleError(`${label} must not traverse a symbolic link`);
|
|
948
|
+
if (!finalPart && !stat.isDirectory())
|
|
949
|
+
throw new KernelCompatLifecycleError(`${label} has a non-directory parent`);
|
|
950
|
+
}
|
|
951
|
+
return candidate;
|
|
952
|
+
}
|
|
953
|
+
function isInsideDirectory(candidate, root) {
|
|
954
|
+
const relative = path.relative(root, candidate);
|
|
955
|
+
return relative.length > 0 && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
956
|
+
}
|
|
957
|
+
function ensureParentDirectories(root, relativePath, uid) {
|
|
958
|
+
const parts = relativePath.split("/");
|
|
959
|
+
if (parts.length === 1)
|
|
960
|
+
return;
|
|
961
|
+
let cursor = root;
|
|
962
|
+
for (const part of parts.slice(0, -1)) {
|
|
963
|
+
cursor = path.join(cursor, part);
|
|
964
|
+
const existing = tryLstat(cursor);
|
|
965
|
+
if (existing === undefined)
|
|
966
|
+
mkdirSync(cursor, { mode: 0o700 });
|
|
967
|
+
assertRealDirectory(cursor, "staged runfiles parent", uid);
|
|
968
|
+
chmodSync(cursor, 0o700);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function mainSymlinkRelativePath(pins) {
|
|
972
|
+
return `${path.posix.dirname(pins.targets.modelSelection.relativePath)}/main`;
|
|
973
|
+
}
|
|
974
|
+
function hardenRunfiles(runfilesPath, releaseRoot, requiredMainLink, uid) {
|
|
975
|
+
let mainLinkTarget;
|
|
976
|
+
const walk = (directory, prefix) => {
|
|
977
|
+
const directoryStat = lstatSync(directory);
|
|
978
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory() || directoryStat.uid !== uid) {
|
|
979
|
+
throw new KernelCompatLifecycleError("unpacked runfiles contain an unsafe directory");
|
|
980
|
+
}
|
|
981
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
982
|
+
const relativePath = prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
983
|
+
const child = path.join(directory, entry.name);
|
|
984
|
+
const stat = lstatSync(child);
|
|
985
|
+
if (stat.uid !== uid) {
|
|
986
|
+
throw new KernelCompatLifecycleError("unpacked runfiles contain a foreign-owned entry");
|
|
987
|
+
}
|
|
988
|
+
if (stat.isSymbolicLink()) {
|
|
989
|
+
const target = resolveSafeRunfilesSymlink(child, releaseRoot, uid);
|
|
990
|
+
if (relativePath === requiredMainLink)
|
|
991
|
+
mainLinkTarget = target;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (entry.isDirectory()) {
|
|
995
|
+
walk(child, relativePath);
|
|
996
|
+
}
|
|
997
|
+
else if (entry.isFile()) {
|
|
998
|
+
const executable = (stat.mode & 0o111) !== 0;
|
|
999
|
+
chmodSync(child, executable ? 0o500 : 0o400);
|
|
1000
|
+
fsyncFile(child);
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
throw new KernelCompatLifecycleError("unpacked runfiles contain a special filesystem entry");
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
chmodSync(directory, 0o500);
|
|
1007
|
+
fsyncDirectory(directory);
|
|
1008
|
+
};
|
|
1009
|
+
walk(runfilesPath, "");
|
|
1010
|
+
if (mainLinkTarget === undefined) {
|
|
1011
|
+
throw new KernelCompatLifecycleError("unpacked runfiles are missing the required acp_server/main symbolic link");
|
|
1012
|
+
}
|
|
1013
|
+
if (mainLinkTarget !== path.join(releaseRoot, KERNEL_EXECUTABLE_FILE)) {
|
|
1014
|
+
throw new KernelCompatLifecycleError("acp_server/main symbolic link must resolve to the staged agy_acp_server");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function resolveSafeRunfilesSymlink(linkPath, releaseRoot, uid) {
|
|
1018
|
+
const resolvedReleaseRoot = path.resolve(releaseRoot);
|
|
1019
|
+
const visited = new Set();
|
|
1020
|
+
let cursor = linkPath;
|
|
1021
|
+
for (let depth = 0; depth < MAX_SYMLINK_CHAIN; depth += 1) {
|
|
1022
|
+
const linkStat = lstatSync(cursor);
|
|
1023
|
+
if (!linkStat.isSymbolicLink() || linkStat.uid !== uid) {
|
|
1024
|
+
throw new KernelCompatLifecycleError("unpacked symbolic link chain is not current-user-owned");
|
|
1025
|
+
}
|
|
1026
|
+
const identity = `${linkStat.dev}:${linkStat.ino}`;
|
|
1027
|
+
if (visited.has(identity))
|
|
1028
|
+
throw new KernelCompatLifecycleError("unpacked symbolic link chain contains a cycle");
|
|
1029
|
+
visited.add(identity);
|
|
1030
|
+
const targetText = readlinkSync(cursor);
|
|
1031
|
+
if (targetText.length === 0 || targetText.includes("\0") || targetText.includes("\\") || path.isAbsolute(targetText)) {
|
|
1032
|
+
throw new KernelCompatLifecycleError("unpacked symbolic link must use a relative target");
|
|
1033
|
+
}
|
|
1034
|
+
const candidate = path.resolve(path.dirname(cursor), targetText);
|
|
1035
|
+
if (!isInsideDirectory(candidate, resolvedReleaseRoot)) {
|
|
1036
|
+
throw new KernelCompatLifecycleError("unpacked symbolic link escapes the staged release root");
|
|
1037
|
+
}
|
|
1038
|
+
assertNoIntermediateSymlinkComponents(resolvedReleaseRoot, candidate);
|
|
1039
|
+
const candidateStat = tryLstat(candidate);
|
|
1040
|
+
if (candidateStat === undefined)
|
|
1041
|
+
throw new KernelCompatLifecycleError("unpacked symbolic link target is broken");
|
|
1042
|
+
if (candidateStat.isSymbolicLink()) {
|
|
1043
|
+
cursor = candidate;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (!candidateStat.isFile() || candidateStat.uid !== uid || (candidateStat.mode & 0o022) !== 0) {
|
|
1047
|
+
throw new KernelCompatLifecycleError("unpacked symbolic link target must be a safe current-user-owned regular file");
|
|
1048
|
+
}
|
|
1049
|
+
return candidate;
|
|
1050
|
+
}
|
|
1051
|
+
throw new KernelCompatLifecycleError("unpacked symbolic link chain cannot be proven safe");
|
|
1052
|
+
}
|
|
1053
|
+
function assertNoIntermediateSymlinkComponents(root, candidate) {
|
|
1054
|
+
const relative = path.relative(root, candidate);
|
|
1055
|
+
const parts = relative.split(path.sep);
|
|
1056
|
+
let cursor = root;
|
|
1057
|
+
for (const part of parts.slice(0, -1)) {
|
|
1058
|
+
cursor = path.join(cursor, part);
|
|
1059
|
+
const stat = tryLstat(cursor);
|
|
1060
|
+
if (stat === undefined || !stat.isDirectory() || stat.isSymbolicLink()) {
|
|
1061
|
+
throw new KernelCompatLifecycleError("unpacked symbolic link chain has an unsafe parent");
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function verifyRelease(releasePath, pins, pinDigest, uid) {
|
|
1066
|
+
assertRealDirectory(releasePath, "prepared release", uid, 0o500);
|
|
1067
|
+
const receipt = readReceipt(releasePath, uid);
|
|
1068
|
+
if (receipt.artifactId !== path.basename(releasePath)) {
|
|
1069
|
+
throw new KernelCompatLifecycleError("prepared release directory does not match its receipt artifact id");
|
|
1070
|
+
}
|
|
1071
|
+
if (receipt.pinDigest !== pinDigest || receipt.profileId !== pins.profileId) {
|
|
1072
|
+
throw new KernelCompatLifecycleError("prepared release uses stale or unknown kernel compatibility pins");
|
|
1073
|
+
}
|
|
1074
|
+
if (receipt.inputs.par.sha256 !== pins.parSha256 || receipt.inputs.externalHarness.sha256 !== pins.externalHarnessSha256) {
|
|
1075
|
+
throw new KernelCompatLifecycleError("prepared release does not attest to the pinned official inputs");
|
|
1076
|
+
}
|
|
1077
|
+
if (receipt.layout.runfilesRelativePath !== RUNFILES_DIRECTORY ||
|
|
1078
|
+
receipt.layout.kernelExecutableRelativePath !== KERNEL_EXECUTABLE_FILE ||
|
|
1079
|
+
receipt.layout.externalHarnessRelativePath !== EXTERNAL_HARNESS_FILE) {
|
|
1080
|
+
throw new KernelCompatLifecycleError("prepared release has an unsupported adjacent runfiles layout");
|
|
1081
|
+
}
|
|
1082
|
+
const runfilesPath = safePathUnderDirectory(releasePath, receipt.layout.runfilesRelativePath, "receipt runfiles", true);
|
|
1083
|
+
assertRealDirectory(runfilesPath, "receipt runfiles", uid, 0o500);
|
|
1084
|
+
// Runtime verification intentionally hashes only bounded patched sources and generated files.
|
|
1085
|
+
for (const target of KERNEL_COMPAT_TARGET_NAMES) {
|
|
1086
|
+
const postimage = receipt.postimages[target];
|
|
1087
|
+
if (postimage.relativePath !== pins.targets[target].relativePath) {
|
|
1088
|
+
throw new KernelCompatLifecycleError(`${target} receipt path does not match current pins`);
|
|
1089
|
+
}
|
|
1090
|
+
const actual = digestSmallPostimage(runfilesPath, postimage, target, uid);
|
|
1091
|
+
if (actual.sha256 !== postimage.sha256 || actual.bytes !== postimage.bytes) {
|
|
1092
|
+
throw new KernelCompatLifecycleError(`${target} postimage hash does not match the receipt`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (receipt.postimages.serverControl.sha256 !== pins.targets.serverControl.preimageSha256) {
|
|
1096
|
+
throw new KernelCompatLifecycleError("server.py receipt hash does not match the immutable control pin");
|
|
1097
|
+
}
|
|
1098
|
+
const compatPostimage = receipt.postimages.compatModule;
|
|
1099
|
+
if (compatPostimage.relativePath !== receipt.layout.compatModuleRelativePath) {
|
|
1100
|
+
throw new KernelCompatLifecycleError("compatibility module receipt path is inconsistent");
|
|
1101
|
+
}
|
|
1102
|
+
const actualCompat = digestSmallPostimage(runfilesPath, compatPostimage, "compatibility module", uid);
|
|
1103
|
+
if (actualCompat.sha256 !== compatPostimage.sha256 || actualCompat.bytes !== compatPostimage.bytes) {
|
|
1104
|
+
throw new KernelCompatLifecycleError("compatibility module postimage hash does not match the receipt");
|
|
1105
|
+
}
|
|
1106
|
+
const executable = safePathUnderDirectory(releasePath, KERNEL_EXECUTABLE_FILE, "receipt kernel executable", true);
|
|
1107
|
+
assertOwnedRegularFile(executable, "receipt kernel executable", uid, true, 0o500);
|
|
1108
|
+
const harness = safePathUnderDirectory(releasePath, EXTERNAL_HARNESS_FILE, "receipt external harness", true);
|
|
1109
|
+
assertOwnedRegularFile(harness, "receipt external harness", uid, true, 0o500);
|
|
1110
|
+
validateImporterEnvironment(runfilesPath, receipt.layout, uid);
|
|
1111
|
+
const wrapperPath = safePathUnderDirectory(releasePath, receipt.wrapper.relativePath, "receipt wrapper", true);
|
|
1112
|
+
const wrapperDigest = digestSmallFile(wrapperPath, "receipt wrapper", uid, true);
|
|
1113
|
+
if (wrapperDigest.sha256 !== receipt.wrapper.sha256 || wrapperDigest.bytes !== receipt.wrapper.bytes) {
|
|
1114
|
+
throw new KernelCompatLifecycleError("receipt wrapper hash does not match the generated wrapper");
|
|
1115
|
+
}
|
|
1116
|
+
const verifierPath = safePathUnderDirectory(releasePath, receipt.wrapper.verifierRelativePath, "receipt wrapper verifier", true);
|
|
1117
|
+
const verifierDigest = digestSmallFile(verifierPath, "receipt wrapper verifier", uid, true);
|
|
1118
|
+
if (verifierDigest.sha256 !== receipt.wrapper.verifierSha256 || verifierDigest.bytes !== receipt.wrapper.verifierBytes) {
|
|
1119
|
+
throw new KernelCompatLifecycleError("receipt wrapper verifier hash does not match the generated verifier");
|
|
1120
|
+
}
|
|
1121
|
+
return { receipt, wrapperPath };
|
|
1122
|
+
}
|
|
1123
|
+
function digestSmallPostimage(runfilesPath, postimage, label, uid) {
|
|
1124
|
+
const filePath = safePathUnderDirectory(runfilesPath, postimage.relativePath, `${label} receipt postimage`, true);
|
|
1125
|
+
return digestSmallFile(filePath, `${label} receipt postimage`, uid, false);
|
|
1126
|
+
}
|
|
1127
|
+
function readReceipt(releasePath, uid) {
|
|
1128
|
+
const receiptPath = safePathUnderDirectory(releasePath, "receipt.json", "receipt", true);
|
|
1129
|
+
let parsed;
|
|
1130
|
+
try {
|
|
1131
|
+
const opened = openOwnedRegularFile(receiptPath, "receipt", uid, false);
|
|
1132
|
+
try {
|
|
1133
|
+
if ((opened.before.mode & 0o777) !== 0o600) {
|
|
1134
|
+
throw new KernelCompatLifecycleError("receipt must be a current-user-owned regular file with mode 0600");
|
|
1135
|
+
}
|
|
1136
|
+
if (opened.before.size > MAX_RECEIPT_BYTES)
|
|
1137
|
+
throw new KernelCompatLifecycleError("receipt exceeds the sanitised size limit");
|
|
1138
|
+
parsed = JSON.parse(readFileSync(opened.fd, "utf8"));
|
|
1139
|
+
assertStableFile(opened.fd, opened.before, "receipt");
|
|
1140
|
+
}
|
|
1141
|
+
finally {
|
|
1142
|
+
closeSync(opened.fd);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
if (error instanceof KernelCompatLifecycleError)
|
|
1147
|
+
throw error;
|
|
1148
|
+
throw new KernelCompatLifecycleError("receipt is not valid JSON");
|
|
1149
|
+
}
|
|
1150
|
+
return validateReceipt(parsed);
|
|
1151
|
+
}
|
|
1152
|
+
function validateReceipt(value) {
|
|
1153
|
+
if (!isRecord(value))
|
|
1154
|
+
throw new KernelCompatLifecycleError("receipt must be an object");
|
|
1155
|
+
if (value.schemaVersion !== RECEIPT_SCHEMA_VERSION)
|
|
1156
|
+
throw new KernelCompatLifecycleError("receipt schema version is unsupported");
|
|
1157
|
+
const artifactId = requireArtifactIdValue(value.artifactId, "receipt artifact id");
|
|
1158
|
+
const profileId = requireArtifactIdValue(value.profileId, "receipt profile id");
|
|
1159
|
+
const createdAt = requireString(value.createdAt, "receipt timestamp");
|
|
1160
|
+
const pinDigest = requireDigestValue(value.pinDigest, "receipt pin digest");
|
|
1161
|
+
const patchDigest = requireDigestValue(value.patchDigest, "receipt patch digest");
|
|
1162
|
+
const inputs = requireRecord(value.inputs, "receipt inputs");
|
|
1163
|
+
const layout = requireRecord(value.layout, "receipt layout");
|
|
1164
|
+
const postimages = requireRecord(value.postimages, "receipt postimages");
|
|
1165
|
+
const wrapper = requireRecord(value.wrapper, "receipt wrapper");
|
|
1166
|
+
const parsedPostimages = {};
|
|
1167
|
+
for (const key of [...KERNEL_COMPAT_TARGET_NAMES, "compatModule"]) {
|
|
1168
|
+
parsedPostimages[key] = parsePostimage(postimages[key], `receipt postimage ${key}`);
|
|
1169
|
+
}
|
|
1170
|
+
const parsedImporterEnvironment = parseImporterEnvironment(layout.importerEnvironment);
|
|
1171
|
+
return {
|
|
1172
|
+
schemaVersion: RECEIPT_SCHEMA_VERSION,
|
|
1173
|
+
artifactId,
|
|
1174
|
+
profileId,
|
|
1175
|
+
createdAt,
|
|
1176
|
+
pinDigest,
|
|
1177
|
+
patchDigest,
|
|
1178
|
+
inputs: {
|
|
1179
|
+
par: parseDigest(inputs.par, "receipt PAR input"),
|
|
1180
|
+
externalHarness: parseDigest(inputs.externalHarness, "receipt external harness input"),
|
|
1181
|
+
compatModule: parseDigest(inputs.compatModule, "receipt compatibility module input")
|
|
1182
|
+
},
|
|
1183
|
+
layout: {
|
|
1184
|
+
runfilesRelativePath: requireSafeRelativeValue(layout.runfilesRelativePath, "receipt runfiles path"),
|
|
1185
|
+
kernelExecutableRelativePath: requireSafeRelativeValue(layout.kernelExecutableRelativePath, "receipt kernel executable path"),
|
|
1186
|
+
externalHarnessRelativePath: requireSafeRelativeValue(layout.externalHarnessRelativePath, "receipt external harness path"),
|
|
1187
|
+
compatModuleRelativePath: requireSafeRelativeValue(layout.compatModuleRelativePath, "receipt compatibility module path"),
|
|
1188
|
+
importerEnvironment: parsedImporterEnvironment
|
|
1189
|
+
},
|
|
1190
|
+
postimages: parsedPostimages,
|
|
1191
|
+
wrapper: {
|
|
1192
|
+
relativePath: requireSafeRelativeValue(wrapper.relativePath, "receipt wrapper path"),
|
|
1193
|
+
sha256: requireDigestValue(wrapper.sha256, "receipt wrapper hash"),
|
|
1194
|
+
bytes: requireByteCount(wrapper.bytes, "receipt wrapper byte count"),
|
|
1195
|
+
verifierRelativePath: requireSafeRelativeValue(wrapper.verifierRelativePath, "receipt wrapper verifier path"),
|
|
1196
|
+
verifierSha256: requireDigestValue(wrapper.verifierSha256, "receipt wrapper verifier hash"),
|
|
1197
|
+
verifierBytes: requireByteCount(wrapper.verifierBytes, "receipt wrapper verifier byte count")
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function parseImporterEnvironment(value) {
|
|
1202
|
+
const record = requireRecord(value, "receipt importer environment");
|
|
1203
|
+
const parsed = {};
|
|
1204
|
+
for (const [name, relativePath] of Object.entries(record)) {
|
|
1205
|
+
if (!ENVIRONMENT_NAME_PATTERN.test(name))
|
|
1206
|
+
throw new KernelCompatLifecycleError("receipt importer environment name is invalid");
|
|
1207
|
+
if (["ANTIGRAVITY_HARNESS_PATH", "PYTHONDONTWRITEBYTECODE", "PYTHONPATH"].includes(name)) {
|
|
1208
|
+
throw new KernelCompatLifecycleError("receipt importer environment overrides a wrapper-controlled value");
|
|
1209
|
+
}
|
|
1210
|
+
parsed[name] = requireSafeRelativeValue(relativePath, `receipt importer environment ${name}`);
|
|
1211
|
+
}
|
|
1212
|
+
return Object.freeze(parsed);
|
|
1213
|
+
}
|
|
1214
|
+
function parseDigest(value, label) {
|
|
1215
|
+
const record = requireRecord(value, label);
|
|
1216
|
+
return { sha256: requireDigestValue(record.sha256, `${label} hash`), bytes: requireByteCount(record.bytes, `${label} byte count`) };
|
|
1217
|
+
}
|
|
1218
|
+
function parsePostimage(value, label) {
|
|
1219
|
+
const record = requireRecord(value, label);
|
|
1220
|
+
return { relativePath: requireSafeRelativeValue(record.relativePath, `${label} path`), ...parseDigest(record, label) };
|
|
1221
|
+
}
|
|
1222
|
+
function isRecord(value) {
|
|
1223
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1224
|
+
}
|
|
1225
|
+
function requireRecord(value, label) {
|
|
1226
|
+
if (!isRecord(value))
|
|
1227
|
+
throw new KernelCompatLifecycleError(`${label} must be an object`);
|
|
1228
|
+
return value;
|
|
1229
|
+
}
|
|
1230
|
+
function requireString(value, label) {
|
|
1231
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
1232
|
+
throw new KernelCompatLifecycleError(`${label} must be a non-empty string`);
|
|
1233
|
+
}
|
|
1234
|
+
return value;
|
|
1235
|
+
}
|
|
1236
|
+
function requireArtifactIdValue(value, label) {
|
|
1237
|
+
if (typeof value !== "string")
|
|
1238
|
+
throw new KernelCompatLifecycleError(`${label} must be an identifier`);
|
|
1239
|
+
return requireIdentifier(label, value);
|
|
1240
|
+
}
|
|
1241
|
+
function requireDigestValue(value, label) {
|
|
1242
|
+
if (typeof value !== "string")
|
|
1243
|
+
throw new KernelCompatLifecycleError(`${label} must be a SHA-256 digest`);
|
|
1244
|
+
requireSha256(label, value);
|
|
1245
|
+
return value;
|
|
1246
|
+
}
|
|
1247
|
+
function requireByteCount(value, label) {
|
|
1248
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
1249
|
+
throw new KernelCompatLifecycleError(`${label} must be a non-negative integer`);
|
|
1250
|
+
}
|
|
1251
|
+
return value;
|
|
1252
|
+
}
|
|
1253
|
+
function requireSafeRelativeValue(value, label) {
|
|
1254
|
+
if (typeof value !== "string")
|
|
1255
|
+
throw new KernelCompatLifecycleError(`${label} must be a relative path`);
|
|
1256
|
+
assertSafeRelativePath(label, value);
|
|
1257
|
+
return value;
|
|
1258
|
+
}
|
|
1259
|
+
function assertIdempotentReceipt(receipt, par, externalHarness, compatModule, patchDigest) {
|
|
1260
|
+
if (receipt.patchDigest !== patchDigest ||
|
|
1261
|
+
receipt.inputs.par.sha256 !== par.sha256 || receipt.inputs.par.bytes !== par.bytes ||
|
|
1262
|
+
receipt.inputs.externalHarness.sha256 !== externalHarness.sha256 || receipt.inputs.externalHarness.bytes !== externalHarness.bytes ||
|
|
1263
|
+
receipt.inputs.compatModule.sha256 !== compatModule.sha256 || receipt.inputs.compatModule.bytes !== compatModule.bytes) {
|
|
1264
|
+
throw new KernelCompatLifecycleError("existing prepared release does not match the current local inputs");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
function buildArtifactId(profileId, parHash, compatibilityVersion, compatHash, patchDigest) {
|
|
1268
|
+
return `${profileId}-${parHash.slice(0, 12)}-${compatibilityVersion}-${compatHash.slice(0, 12)}-${patchDigest.slice(0, 12)}`;
|
|
1269
|
+
}
|
|
1270
|
+
function digestPatchPlan(plan) {
|
|
1271
|
+
return sha256Text(canonicalJson({
|
|
1272
|
+
edits: plan.edits.map((edit) => ({
|
|
1273
|
+
id: edit.id,
|
|
1274
|
+
target: edit.target,
|
|
1275
|
+
findSha256: sha256Text(edit.find),
|
|
1276
|
+
replacementSha256: sha256Text(edit.replacement)
|
|
1277
|
+
}))
|
|
1278
|
+
}));
|
|
1279
|
+
}
|
|
1280
|
+
function sha256Text(value) {
|
|
1281
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
1282
|
+
}
|
|
1283
|
+
function canonicalJson(value) {
|
|
1284
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1285
|
+
return JSON.stringify(value);
|
|
1286
|
+
}
|
|
1287
|
+
if (Array.isArray(value))
|
|
1288
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
1289
|
+
if (isRecord(value)) {
|
|
1290
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
1291
|
+
}
|
|
1292
|
+
throw new KernelCompatLifecycleError("cannot canonicalise an undefined receipt value");
|
|
1293
|
+
}
|
|
1294
|
+
function withWriterLock(root, uid, clock, operation) {
|
|
1295
|
+
const lockPath = path.join(root, LOCK_FILE);
|
|
1296
|
+
const identity = observeCurrentProcessIdentity();
|
|
1297
|
+
let fd;
|
|
1298
|
+
for (let attempts = 0; attempts < 4; attempts += 1) {
|
|
1299
|
+
try {
|
|
1300
|
+
fd = openSync(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | requiredNoFollow() | CLOEXEC, 0o600);
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
catch (error) {
|
|
1304
|
+
if (!isAlreadyExists(error))
|
|
1305
|
+
throw error;
|
|
1306
|
+
recoverStaleLock(root, lockPath, uid, currentClockMilliseconds(clock));
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (fd === undefined)
|
|
1310
|
+
throw new KernelCompatLifecycleError("could not replace a stale kernel compatibility lifecycle lock");
|
|
1311
|
+
const lockStat = fstatSync(fd);
|
|
1312
|
+
try {
|
|
1313
|
+
if (!lockStat.isFile() || lockStat.uid !== uid || (lockStat.mode & 0o777) !== 0o600) {
|
|
1314
|
+
throw new KernelCompatLifecycleError("could not establish a safe lifecycle lock");
|
|
1315
|
+
}
|
|
1316
|
+
writeAll(fd, Buffer.from(`${canonicalJson({ schemaVersion: LOCK_SCHEMA_VERSION, ...identity })}\n`, "utf8"));
|
|
1317
|
+
fsyncSync(fd);
|
|
1318
|
+
fsyncDirectory(root);
|
|
1319
|
+
return operation();
|
|
1320
|
+
}
|
|
1321
|
+
finally {
|
|
1322
|
+
closeSync(fd);
|
|
1323
|
+
const current = tryLstat(lockPath);
|
|
1324
|
+
if (current !== undefined && current.isFile() && current.dev === lockStat.dev && current.ino === lockStat.ino && current.uid === uid) {
|
|
1325
|
+
unlinkSync(lockPath);
|
|
1326
|
+
fsyncDirectory(root);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function currentClockMilliseconds(clock) {
|
|
1331
|
+
const value = clock().getTime();
|
|
1332
|
+
if (!Number.isFinite(value))
|
|
1333
|
+
throw new KernelCompatLifecycleError("lifecycle clock returned an invalid timestamp");
|
|
1334
|
+
return value;
|
|
1335
|
+
}
|
|
1336
|
+
function observeCurrentProcessIdentity() {
|
|
1337
|
+
return { pid: process.pid, procStartTime: readLinuxProcessStartTime(process.pid, true) };
|
|
1338
|
+
}
|
|
1339
|
+
function readLinuxProcessStartTime(pid, required) {
|
|
1340
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
1341
|
+
throw new KernelCompatLifecycleError("lifecycle lock pid is invalid");
|
|
1342
|
+
}
|
|
1343
|
+
let statText;
|
|
1344
|
+
try {
|
|
1345
|
+
statText = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
1346
|
+
}
|
|
1347
|
+
catch (error) {
|
|
1348
|
+
if (!required && isMissing(error))
|
|
1349
|
+
return "";
|
|
1350
|
+
throw new KernelCompatLifecycleError("could not inspect Linux process identity for lifecycle lock recovery");
|
|
1351
|
+
}
|
|
1352
|
+
const closingParen = statText.lastIndexOf(")");
|
|
1353
|
+
if (closingParen < 0)
|
|
1354
|
+
throw new KernelCompatLifecycleError("could not parse Linux process identity for lifecycle lock recovery");
|
|
1355
|
+
const fields = statText.slice(closingParen + 2).trim().split(/\s+/);
|
|
1356
|
+
const startTime = fields[19];
|
|
1357
|
+
if (startTime === undefined || !INTEGER_PATTERN.test(startTime)) {
|
|
1358
|
+
throw new KernelCompatLifecycleError("could not parse Linux process start time for lifecycle lock recovery");
|
|
1359
|
+
}
|
|
1360
|
+
return startTime;
|
|
1361
|
+
}
|
|
1362
|
+
function recoverStaleLock(root, lockPath, uid, nowMilliseconds) {
|
|
1363
|
+
const existing = readLockReceipt(lockPath, uid, nowMilliseconds);
|
|
1364
|
+
if (existing === undefined)
|
|
1365
|
+
return;
|
|
1366
|
+
if (existing.receipt !== undefined) {
|
|
1367
|
+
const observedStartTime = readLinuxProcessStartTime(existing.receipt.pid, false);
|
|
1368
|
+
if (observedStartTime !== "" && observedStartTime === existing.receipt.procStartTime) {
|
|
1369
|
+
throw new KernelCompatLifecycleError("kernel compatibility lifecycle is already running");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
else if (isIncompleteLockWithinGrace(existing.stat, nowMilliseconds)) {
|
|
1373
|
+
throw new KernelCompatLifecycleError("kernel compatibility lifecycle lock is incomplete and within the recovery grace period");
|
|
1374
|
+
}
|
|
1375
|
+
quarantineStaleLock(root, lockPath, uid, existing.stat);
|
|
1376
|
+
}
|
|
1377
|
+
function isIncompleteLockWithinGrace(stat, nowMilliseconds) {
|
|
1378
|
+
const ageMilliseconds = nowMilliseconds - stat.mtimeMs;
|
|
1379
|
+
return !Number.isFinite(ageMilliseconds) || ageMilliseconds < LOCK_INCOMPLETE_GRACE_MS;
|
|
1380
|
+
}
|
|
1381
|
+
function quarantineStaleLock(root, lockPath, uid, expected) {
|
|
1382
|
+
const before = tryLstat(lockPath);
|
|
1383
|
+
if (before === undefined)
|
|
1384
|
+
return;
|
|
1385
|
+
if (before.isSymbolicLink() || !before.isFile() || before.uid !== uid || (before.mode & 0o777) !== 0o600) {
|
|
1386
|
+
throw new KernelCompatLifecycleError("existing lifecycle lock is unsafe and cannot be recovered");
|
|
1387
|
+
}
|
|
1388
|
+
if (!sameFileSnapshot(before, expected)) {
|
|
1389
|
+
throw new KernelCompatLifecycleError("existing lifecycle lock changed during recovery");
|
|
1390
|
+
}
|
|
1391
|
+
const quarantinePath = path.join(root, `${LOCK_FILE}.stale-${randomBytes(8).toString("hex")}`);
|
|
1392
|
+
renameSync(lockPath, quarantinePath);
|
|
1393
|
+
const moved = lstatSync(quarantinePath);
|
|
1394
|
+
if (!moved.isFile() || moved.isSymbolicLink() || moved.uid !== uid || moved.dev !== before.dev || moved.ino !== before.ino) {
|
|
1395
|
+
throw new KernelCompatLifecycleError("lifecycle lock changed during stale-lock quarantine");
|
|
1396
|
+
}
|
|
1397
|
+
fsyncDirectory(root);
|
|
1398
|
+
}
|
|
1399
|
+
function readLockReceipt(lockPath, uid, nowMilliseconds) {
|
|
1400
|
+
const stat = tryLstat(lockPath);
|
|
1401
|
+
if (stat === undefined)
|
|
1402
|
+
return undefined;
|
|
1403
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.uid !== uid) {
|
|
1404
|
+
throw new KernelCompatLifecycleError("existing lifecycle lock is unsafe and cannot be recovered");
|
|
1405
|
+
}
|
|
1406
|
+
if ((stat.mode & 0o777) !== 0o600) {
|
|
1407
|
+
if (isIncompleteLockWithinGrace(stat, nowMilliseconds)) {
|
|
1408
|
+
throw new KernelCompatLifecycleError("kernel compatibility lifecycle lock is incomplete and within the recovery grace period");
|
|
1409
|
+
}
|
|
1410
|
+
throw new KernelCompatLifecycleError("existing lifecycle lock is unsafe and cannot be recovered");
|
|
1411
|
+
}
|
|
1412
|
+
if (stat.size > MAX_LOCK_BYTES)
|
|
1413
|
+
return { stat };
|
|
1414
|
+
let parsed;
|
|
1415
|
+
try {
|
|
1416
|
+
const opened = openOwnedRegularFile(lockPath, "existing lifecycle lock", uid, false);
|
|
1417
|
+
try {
|
|
1418
|
+
if (!sameFileSnapshot(opened.before, stat)) {
|
|
1419
|
+
throw new KernelCompatLifecycleError("existing lifecycle lock changed while it was being opened");
|
|
1420
|
+
}
|
|
1421
|
+
if (opened.before.size > MAX_LOCK_BYTES)
|
|
1422
|
+
throw new KernelCompatLifecycleError("existing lifecycle lock exceeds the safe size limit");
|
|
1423
|
+
parsed = JSON.parse(readFileSync(opened.fd, "utf8"));
|
|
1424
|
+
assertStableFile(opened.fd, opened.before, "existing lifecycle lock");
|
|
1425
|
+
}
|
|
1426
|
+
finally {
|
|
1427
|
+
closeSync(opened.fd);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
catch (error) {
|
|
1431
|
+
if (error instanceof SyntaxError)
|
|
1432
|
+
return { stat };
|
|
1433
|
+
if (error instanceof KernelCompatLifecycleError)
|
|
1434
|
+
throw error;
|
|
1435
|
+
return { stat };
|
|
1436
|
+
}
|
|
1437
|
+
if (!isRecord(parsed) || parsed.schemaVersion !== LOCK_SCHEMA_VERSION || typeof parsed.pid !== "number" ||
|
|
1438
|
+
!Number.isSafeInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.procStartTime !== "string" ||
|
|
1439
|
+
!INTEGER_PATTERN.test(parsed.procStartTime)) {
|
|
1440
|
+
return { stat };
|
|
1441
|
+
}
|
|
1442
|
+
return {
|
|
1443
|
+
stat,
|
|
1444
|
+
receipt: { schemaVersion: LOCK_SCHEMA_VERSION, pid: parsed.pid, procStartTime: parsed.procStartTime }
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
function stableWrapperPathFor(root) {
|
|
1448
|
+
return path.join(root, STABLE_WRAPPER_FILE);
|
|
1449
|
+
}
|
|
1450
|
+
function ensureStableActivationWrapper(root, pinDigest, uid) {
|
|
1451
|
+
writeOrReplaceOwnedExecutable(root, ACTIVE_RESOLVER_FILE, renderActiveResolver(pinDigest), uid);
|
|
1452
|
+
writeOrReplaceOwnedExecutable(root, STABLE_WRAPPER_FILE, renderStableWrapper(), uid);
|
|
1453
|
+
}
|
|
1454
|
+
function writeOrReplaceOwnedExecutable(root, filename, content, uid) {
|
|
1455
|
+
const target = path.join(root, filename);
|
|
1456
|
+
const existing = tryLstat(target);
|
|
1457
|
+
if (existing !== undefined) {
|
|
1458
|
+
assertOwnedRegularFile(target, `stable ${filename}`, uid, true, 0o500);
|
|
1459
|
+
const actual = readSmallTextFile(target, `stable ${filename}`, uid);
|
|
1460
|
+
if (actual === content)
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const temporary = path.join(root, `.${filename}-${randomBytes(8).toString("hex")}`);
|
|
1464
|
+
try {
|
|
1465
|
+
writeNewFile(temporary, content, 0o500);
|
|
1466
|
+
renameSync(temporary, target);
|
|
1467
|
+
fsyncDirectory(root);
|
|
1468
|
+
}
|
|
1469
|
+
finally {
|
|
1470
|
+
const leftover = tryLstat(temporary);
|
|
1471
|
+
if (leftover !== undefined && leftover.isFile() && !leftover.isSymbolicLink() && leftover.uid === uid)
|
|
1472
|
+
unlinkSync(temporary);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function commitActivationState(root, state, pinDigest, uid, hooks) {
|
|
1476
|
+
const activationStates = ensureInternalDirectory(root, ACTIVATION_STATES_DIRECTORY, uid);
|
|
1477
|
+
const stateId = `activation-${randomBytes(12).toString("hex")}`;
|
|
1478
|
+
const temporaryState = mkdtempSync(path.join(activationStates, ".activation-stage-"), "utf8");
|
|
1479
|
+
chmodSync(temporaryState, 0o700);
|
|
1480
|
+
const complete = {
|
|
1481
|
+
schemaVersion: ACTIVATION_STATE_SCHEMA_VERSION,
|
|
1482
|
+
pinDigest,
|
|
1483
|
+
currentArtifactId: requireArtifactId(state.currentArtifactId),
|
|
1484
|
+
...(state.previousArtifactId === undefined ? {} : { previousArtifactId: requireArtifactId(state.previousArtifactId) })
|
|
1485
|
+
};
|
|
1486
|
+
try {
|
|
1487
|
+
writeNewFile(path.join(temporaryState, "activation.json"), `${canonicalJson(complete)}\n`, 0o400);
|
|
1488
|
+
chmodSync(temporaryState, 0o500);
|
|
1489
|
+
fsyncDirectory(temporaryState);
|
|
1490
|
+
const finalStatePath = path.join(activationStates, stateId);
|
|
1491
|
+
renameSync(temporaryState, finalStatePath);
|
|
1492
|
+
fsyncDirectory(activationStates);
|
|
1493
|
+
hooks.beforeActivationCommit?.(Object.freeze({ ...state }));
|
|
1494
|
+
const temporaryReference = path.join(root, `.${ACTIVE_REFERENCE}-${randomBytes(8).toString("hex")}`);
|
|
1495
|
+
try {
|
|
1496
|
+
symlinkSync(`${ACTIVATION_STATES_DIRECTORY}/${stateId}`, temporaryReference);
|
|
1497
|
+
fsyncDirectory(root);
|
|
1498
|
+
renameSync(temporaryReference, path.join(root, ACTIVE_REFERENCE));
|
|
1499
|
+
fsyncDirectory(root);
|
|
1500
|
+
}
|
|
1501
|
+
finally {
|
|
1502
|
+
removeTemporarySymlink(root, temporaryReference, uid);
|
|
1503
|
+
}
|
|
1504
|
+
hooks.afterActivationCommit?.(Object.freeze({ ...state }));
|
|
1505
|
+
}
|
|
1506
|
+
finally {
|
|
1507
|
+
const leftover = tryLstat(temporaryState);
|
|
1508
|
+
if (leftover !== undefined && leftover.isDirectory() && !leftover.isSymbolicLink() && leftover.uid === uid) {
|
|
1509
|
+
removeDirectoryIfSafe(activationStates, temporaryState, ".activation-stage-", uid);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function removeTemporarySymlink(root, temporaryPath, uid) {
|
|
1514
|
+
if (path.dirname(temporaryPath) !== root)
|
|
1515
|
+
return;
|
|
1516
|
+
const stat = tryLstat(temporaryPath);
|
|
1517
|
+
if (stat !== undefined && stat.isSymbolicLink() && stat.uid === uid)
|
|
1518
|
+
unlinkSync(temporaryPath);
|
|
1519
|
+
}
|
|
1520
|
+
function requireActiveState(root, uid, pinDigest) {
|
|
1521
|
+
const active = readActiveState(root, uid, pinDigest);
|
|
1522
|
+
if (active === undefined)
|
|
1523
|
+
throw new KernelCompatLifecycleError("active compatibility release is not set");
|
|
1524
|
+
return active;
|
|
1525
|
+
}
|
|
1526
|
+
function readActiveState(root, uid, expectedPinDigest) {
|
|
1527
|
+
const reference = readActiveStateReference(root, uid);
|
|
1528
|
+
if (reference === undefined)
|
|
1529
|
+
return undefined;
|
|
1530
|
+
const state = readActivationStateFile(reference.stateDirectory, uid);
|
|
1531
|
+
if (expectedPinDigest !== undefined && state.pinDigest !== expectedPinDigest) {
|
|
1532
|
+
throw new KernelCompatLifecycleError("active compatibility state uses stale or unknown kernel compatibility pins");
|
|
1533
|
+
}
|
|
1534
|
+
return state;
|
|
1535
|
+
}
|
|
1536
|
+
function readActiveStateReference(root, uid) {
|
|
1537
|
+
const referencePath = path.join(root, ACTIVE_REFERENCE);
|
|
1538
|
+
const linkStat = tryLstat(referencePath);
|
|
1539
|
+
if (linkStat === undefined)
|
|
1540
|
+
return undefined;
|
|
1541
|
+
if (!linkStat.isSymbolicLink() || linkStat.uid !== uid) {
|
|
1542
|
+
throw new KernelCompatLifecycleError("active compatibility state must be a current-user-owned symbolic link");
|
|
1543
|
+
}
|
|
1544
|
+
const target = readlinkSync(referencePath);
|
|
1545
|
+
const prefix = `${ACTIVATION_STATES_DIRECTORY}/`;
|
|
1546
|
+
if (path.isAbsolute(target) || !target.startsWith(prefix) || target.slice(prefix.length).includes("/")) {
|
|
1547
|
+
throw new KernelCompatLifecycleError("active compatibility state points outside managed activation states");
|
|
1548
|
+
}
|
|
1549
|
+
const stateId = requireIdentifier("activation state id", target.slice(prefix.length));
|
|
1550
|
+
const states = safePathUnderDirectory(root, ACTIVATION_STATES_DIRECTORY, "activation states directory", true);
|
|
1551
|
+
assertRealDirectory(states, "activation states directory", uid, 0o700);
|
|
1552
|
+
const stateDirectory = safePathUnderDirectory(states, stateId, "activation state directory", true);
|
|
1553
|
+
assertRealDirectory(stateDirectory, "activation state directory", uid, 0o500);
|
|
1554
|
+
return { stateId, stateDirectory };
|
|
1555
|
+
}
|
|
1556
|
+
function readActivationStateFile(stateDirectory, uid) {
|
|
1557
|
+
const statePath = safePathUnderDirectory(stateDirectory, "activation.json", "activation state", true);
|
|
1558
|
+
let parsed;
|
|
1559
|
+
try {
|
|
1560
|
+
const opened = openOwnedRegularFile(statePath, "activation state", uid, false);
|
|
1561
|
+
try {
|
|
1562
|
+
if ((opened.before.mode & 0o777) !== 0o400) {
|
|
1563
|
+
throw new KernelCompatLifecycleError("activation state must be a current-user-owned regular file with mode 0400");
|
|
1564
|
+
}
|
|
1565
|
+
if (opened.before.size > MAX_ACTIVATION_STATE_BYTES) {
|
|
1566
|
+
throw new KernelCompatLifecycleError("activation state exceeds the safe size limit");
|
|
1567
|
+
}
|
|
1568
|
+
parsed = JSON.parse(readFileSync(opened.fd, "utf8"));
|
|
1569
|
+
assertStableFile(opened.fd, opened.before, "activation state");
|
|
1570
|
+
}
|
|
1571
|
+
finally {
|
|
1572
|
+
closeSync(opened.fd);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
catch (error) {
|
|
1576
|
+
if (error instanceof KernelCompatLifecycleError)
|
|
1577
|
+
throw error;
|
|
1578
|
+
throw new KernelCompatLifecycleError("activation state is not valid JSON");
|
|
1579
|
+
}
|
|
1580
|
+
if (!isRecord(parsed) || parsed.schemaVersion !== ACTIVATION_STATE_SCHEMA_VERSION) {
|
|
1581
|
+
throw new KernelCompatLifecycleError("activation state schema is unsupported");
|
|
1582
|
+
}
|
|
1583
|
+
const currentArtifactId = requireArtifactIdValue(parsed.currentArtifactId, "activation current artifact id");
|
|
1584
|
+
const previousArtifactId = parsed.previousArtifactId === undefined
|
|
1585
|
+
? undefined
|
|
1586
|
+
: requireArtifactIdValue(parsed.previousArtifactId, "activation previous artifact id");
|
|
1587
|
+
const pinDigest = requireDigestValue(parsed.pinDigest, "activation pin digest");
|
|
1588
|
+
if (previousArtifactId === currentArtifactId) {
|
|
1589
|
+
throw new KernelCompatLifecycleError("activation state cannot use the same current and previous artifact");
|
|
1590
|
+
}
|
|
1591
|
+
return {
|
|
1592
|
+
schemaVersion: ACTIVATION_STATE_SCHEMA_VERSION,
|
|
1593
|
+
pinDigest,
|
|
1594
|
+
currentArtifactId,
|
|
1595
|
+
...(previousArtifactId === undefined ? {} : { previousArtifactId })
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
function releasePathFor(root, artifactId, uid) {
|
|
1599
|
+
const releases = safePathUnderDirectory(root, RELEASES_DIRECTORY, "managed releases directory", true);
|
|
1600
|
+
assertRealDirectory(releases, "managed releases directory", uid, 0o700);
|
|
1601
|
+
const releasePath = safePathUnderDirectory(releases, requireArtifactId(artifactId), "prepared release", true);
|
|
1602
|
+
assertRealDirectory(releasePath, "prepared release", uid, 0o500);
|
|
1603
|
+
return releasePath;
|
|
1604
|
+
}
|
|
1605
|
+
function cleanupUnlocked(root, uid, options) {
|
|
1606
|
+
const removedArtifactIds = [];
|
|
1607
|
+
const removedStagingEntries = [];
|
|
1608
|
+
const removedActivationStateDirectories = [];
|
|
1609
|
+
const removedStaleLockQuarantineFiles = [];
|
|
1610
|
+
const skippedEntries = [];
|
|
1611
|
+
// Read without enforcing the current pin so stale builds still protect their live pair.
|
|
1612
|
+
const activeReference = readActiveStateReference(root, uid);
|
|
1613
|
+
const active = activeReference === undefined ? undefined : readActivationStateFile(activeReference.stateDirectory, uid);
|
|
1614
|
+
const protectedIds = new Set([active?.currentArtifactId, active?.previousArtifactId].filter((value) => value !== undefined));
|
|
1615
|
+
const releases = safePathUnderDirectory(root, RELEASES_DIRECTORY, "managed releases directory", false);
|
|
1616
|
+
if (tryLstat(releases) !== undefined) {
|
|
1617
|
+
assertRealDirectory(releases, "managed releases directory", uid, 0o700);
|
|
1618
|
+
for (const entry of readdirSync(releases, { withFileTypes: true })) {
|
|
1619
|
+
if (!IDENTIFIER_PATTERN.test(entry.name)) {
|
|
1620
|
+
skippedEntries.push(`release:${entry.name}`);
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
const candidate = path.join(releases, entry.name);
|
|
1624
|
+
const stat = lstatSync(candidate);
|
|
1625
|
+
if (!entry.isDirectory() || stat.isSymbolicLink() || stat.uid !== uid) {
|
|
1626
|
+
skippedEntries.push(`release:${entry.name}`);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (protectedIds.has(entry.name))
|
|
1630
|
+
continue;
|
|
1631
|
+
if (!options.removeUnreferenced) {
|
|
1632
|
+
skippedEntries.push(`release:${entry.name}`);
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
try {
|
|
1636
|
+
const receipt = readReceipt(candidate, uid);
|
|
1637
|
+
if (receipt.artifactId !== entry.name)
|
|
1638
|
+
throw new KernelCompatLifecycleError("artifact id mismatch");
|
|
1639
|
+
makeTreeWritableForRemoval(candidate, uid);
|
|
1640
|
+
rmSync(candidate, { recursive: true, force: false, maxRetries: 0 });
|
|
1641
|
+
removedArtifactIds.push(entry.name);
|
|
1642
|
+
}
|
|
1643
|
+
catch {
|
|
1644
|
+
skippedEntries.push(`release:${entry.name}`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
fsyncDirectory(releases);
|
|
1648
|
+
}
|
|
1649
|
+
cleanupOrphanActivationStateDirectories(root, uid, activeReference?.stateId, removedActivationStateDirectories, skippedEntries);
|
|
1650
|
+
const staging = safePathUnderDirectory(root, STAGING_DIRECTORY, "staging directory", false);
|
|
1651
|
+
if (tryLstat(staging) !== undefined) {
|
|
1652
|
+
assertRealDirectory(staging, "staging directory", uid, 0o700);
|
|
1653
|
+
for (const entry of readdirSync(staging, { withFileTypes: true })) {
|
|
1654
|
+
if (!entry.name.startsWith("kernel-compat-stage-")) {
|
|
1655
|
+
skippedEntries.push(`staging:${entry.name}`);
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
const candidate = path.join(staging, entry.name);
|
|
1659
|
+
const stat = lstatSync(candidate);
|
|
1660
|
+
if (!entry.isDirectory() || stat.isSymbolicLink() || stat.uid !== uid) {
|
|
1661
|
+
skippedEntries.push(`staging:${entry.name}`);
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
makeTreeWritableForRemoval(candidate, uid);
|
|
1666
|
+
rmSync(candidate, { recursive: true, force: false, maxRetries: 0 });
|
|
1667
|
+
removedStagingEntries.push(entry.name);
|
|
1668
|
+
}
|
|
1669
|
+
catch {
|
|
1670
|
+
skippedEntries.push(`staging:${entry.name}`);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
fsyncDirectory(staging);
|
|
1674
|
+
}
|
|
1675
|
+
cleanupStaleLockQuarantineFiles(root, uid, removedStaleLockQuarantineFiles, skippedEntries);
|
|
1676
|
+
return {
|
|
1677
|
+
removedArtifactIds,
|
|
1678
|
+
removedStagingEntries,
|
|
1679
|
+
removedActivationStateDirectories,
|
|
1680
|
+
removedStaleLockQuarantineFiles,
|
|
1681
|
+
skippedEntries
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function cleanupOrphanActivationStateDirectories(root, uid, activeStateId, removed, skippedEntries) {
|
|
1685
|
+
const states = safePathUnderDirectory(root, ACTIVATION_STATES_DIRECTORY, "activation states directory", false);
|
|
1686
|
+
if (tryLstat(states) === undefined)
|
|
1687
|
+
return;
|
|
1688
|
+
assertRealDirectory(states, "activation states directory", uid, 0o700);
|
|
1689
|
+
for (const entry of readdirSync(states, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
1690
|
+
if (entry.name === activeStateId)
|
|
1691
|
+
continue;
|
|
1692
|
+
if (!MANAGED_ACTIVATION_STATE_PATTERN.test(entry.name)) {
|
|
1693
|
+
skippedEntries.push(`activation-state:${entry.name}`);
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
const candidate = path.join(states, entry.name);
|
|
1697
|
+
const stat = lstatSync(candidate);
|
|
1698
|
+
if (!entry.isDirectory() || stat.isSymbolicLink() || stat.uid !== uid) {
|
|
1699
|
+
skippedEntries.push(`activation-state:${entry.name}`);
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
try {
|
|
1703
|
+
makeTreeWritableForRemoval(candidate, uid);
|
|
1704
|
+
rmSync(candidate, { recursive: true, force: false, maxRetries: 0 });
|
|
1705
|
+
removed.push(entry.name);
|
|
1706
|
+
}
|
|
1707
|
+
catch {
|
|
1708
|
+
skippedEntries.push(`activation-state:${entry.name}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
fsyncDirectory(states);
|
|
1712
|
+
}
|
|
1713
|
+
function cleanupStaleLockQuarantineFiles(root, uid, removed, skippedEntries) {
|
|
1714
|
+
for (const entry of readdirSync(root, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
1715
|
+
if (!STALE_LOCK_QUARANTINE_PATTERN.test(entry.name))
|
|
1716
|
+
continue;
|
|
1717
|
+
const candidate = path.join(root, entry.name);
|
|
1718
|
+
const stat = lstatSync(candidate);
|
|
1719
|
+
if (!entry.isFile() || stat.isSymbolicLink() || stat.uid !== uid || (stat.mode & 0o777) !== 0o600) {
|
|
1720
|
+
skippedEntries.push(`stale-lock:${entry.name}`);
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
try {
|
|
1724
|
+
unlinkSync(candidate);
|
|
1725
|
+
removed.push(entry.name);
|
|
1726
|
+
}
|
|
1727
|
+
catch {
|
|
1728
|
+
skippedEntries.push(`stale-lock:${entry.name}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (removed.length > 0)
|
|
1732
|
+
fsyncDirectory(root);
|
|
1733
|
+
}
|
|
1734
|
+
function listSafeStagingEntries(root, uid, issues) {
|
|
1735
|
+
try {
|
|
1736
|
+
const staging = safePathUnderDirectory(root, STAGING_DIRECTORY, "staging directory", false);
|
|
1737
|
+
if (tryLstat(staging) === undefined)
|
|
1738
|
+
return [];
|
|
1739
|
+
assertRealDirectory(staging, "staging directory", uid, 0o700);
|
|
1740
|
+
return readdirSync(staging, { withFileTypes: true })
|
|
1741
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("kernel-compat-stage-"))
|
|
1742
|
+
.map((entry) => entry.name)
|
|
1743
|
+
.sort();
|
|
1744
|
+
}
|
|
1745
|
+
catch (error) {
|
|
1746
|
+
issues.push(`staging: ${messageFor(error)}`);
|
|
1747
|
+
return [];
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
function removeDirectoryIfSafe(parent, directory, requiredPrefix, uid) {
|
|
1751
|
+
try {
|
|
1752
|
+
if (path.dirname(directory) !== parent || !path.basename(directory).startsWith(requiredPrefix))
|
|
1753
|
+
return;
|
|
1754
|
+
const stat = tryLstat(directory);
|
|
1755
|
+
if (stat !== undefined && stat.isDirectory() && !stat.isSymbolicLink() && stat.uid === uid) {
|
|
1756
|
+
makeTreeWritableForRemoval(directory, uid);
|
|
1757
|
+
rmSync(directory, { recursive: true, force: false, maxRetries: 0 });
|
|
1758
|
+
fsyncDirectory(parent);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
catch {
|
|
1762
|
+
// Preserve a failed stage for explicit safe cleanup rather than hiding the primary result.
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function makeTreeWritableForRemoval(directory, uid) {
|
|
1766
|
+
const stat = lstatSync(directory);
|
|
1767
|
+
if (stat.isSymbolicLink()) {
|
|
1768
|
+
if (stat.uid !== uid)
|
|
1769
|
+
throw new KernelCompatLifecycleError("cleanup target contains a foreign-owned symbolic link");
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (!stat.isDirectory() || stat.uid !== uid) {
|
|
1773
|
+
throw new KernelCompatLifecycleError("cleanup target is not a safe current-user-owned directory");
|
|
1774
|
+
}
|
|
1775
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
1776
|
+
const candidate = path.join(directory, entry.name);
|
|
1777
|
+
const child = lstatSync(candidate);
|
|
1778
|
+
if (child.uid !== uid)
|
|
1779
|
+
throw new KernelCompatLifecycleError("cleanup target contains a foreign-owned entry");
|
|
1780
|
+
if (child.isSymbolicLink())
|
|
1781
|
+
continue;
|
|
1782
|
+
if (entry.isDirectory()) {
|
|
1783
|
+
makeTreeWritableForRemoval(candidate, uid);
|
|
1784
|
+
}
|
|
1785
|
+
else if (entry.isFile()) {
|
|
1786
|
+
chmodSync(candidate, 0o600);
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
throw new KernelCompatLifecycleError("cleanup target contains a special filesystem entry");
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
chmodSync(directory, 0o700);
|
|
1793
|
+
}
|
|
1794
|
+
function fsyncFile(filePath) {
|
|
1795
|
+
const fd = openSync(filePath, constants.O_RDONLY | requiredNoFollow() | CLOEXEC);
|
|
1796
|
+
try {
|
|
1797
|
+
fsyncSync(fd);
|
|
1798
|
+
}
|
|
1799
|
+
finally {
|
|
1800
|
+
closeSync(fd);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function fsyncDirectory(directory) {
|
|
1804
|
+
const fd = openSync(directory, constants.O_RDONLY | DIRECTORY | requiredNoFollow() | CLOEXEC);
|
|
1805
|
+
try {
|
|
1806
|
+
fsyncSync(fd);
|
|
1807
|
+
}
|
|
1808
|
+
finally {
|
|
1809
|
+
closeSync(fd);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function tryLstat(filePath) {
|
|
1813
|
+
try {
|
|
1814
|
+
return lstatSync(filePath);
|
|
1815
|
+
}
|
|
1816
|
+
catch (error) {
|
|
1817
|
+
if (isMissing(error))
|
|
1818
|
+
return undefined;
|
|
1819
|
+
throw error;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
function isMissing(error) {
|
|
1823
|
+
return isNodeError(error) && error.code === "ENOENT";
|
|
1824
|
+
}
|
|
1825
|
+
function isAlreadyExists(error) {
|
|
1826
|
+
return isNodeError(error) && error.code === "EEXIST";
|
|
1827
|
+
}
|
|
1828
|
+
function isNodeError(error) {
|
|
1829
|
+
return error instanceof Error && "code" in error;
|
|
1830
|
+
}
|
|
1831
|
+
function messageFor(error) {
|
|
1832
|
+
return error instanceof Error ? error.message : "unknown kernel compatibility error";
|
|
1833
|
+
}
|
|
1834
|
+
function renderReleaseWrapperVerifier(expectedPinDigest) {
|
|
1835
|
+
return [
|
|
1836
|
+
"#!/usr/bin/env node",
|
|
1837
|
+
"import { createHash } from 'node:crypto';",
|
|
1838
|
+
"import { lstatSync, readFileSync } from 'node:fs';",
|
|
1839
|
+
"import path from 'node:path';",
|
|
1840
|
+
`const EXPECTED_PIN_DIGEST = ${JSON.stringify(expectedPinDigest)};`,
|
|
1841
|
+
"const MAX_SMALL_FILE_BYTES = 16 * 1024 * 1024;",
|
|
1842
|
+
"function fail(message) { process.stderr.write('agy-acp kernel compatibility wrapper refused to run: ' + message + '\\n'); process.exit(1); }",
|
|
1843
|
+
"function safeRelative(value) { return typeof value === 'string' && value.length > 0 && !value.includes('\\0') && !value.includes('\\\\') && !path.isAbsolute(value) && path.posix.normalize(value) === value && !value.split('/').some((part) => part.length === 0 || part === '.' || part === '..'); }",
|
|
1844
|
+
"function safePath(root, relative, label) { if (!safeRelative(relative)) throw new Error(label + ' has an unsafe relative path'); const resolvedRoot = path.resolve(root); let cursor = resolvedRoot; for (const part of relative.split('/')) { cursor = path.join(cursor, part); const stat = lstatSync(cursor); if (stat.isSymbolicLink()) throw new Error(label + ' traverses a symbolic link'); if (cursor !== path.resolve(resolvedRoot, relative) && !stat.isDirectory()) throw new Error(label + ' has a non-directory parent'); } return cursor; }",
|
|
1845
|
+
"function ownedFile(filePath, label, uid, executable, exactMode) { const stat = lstatSync(filePath); if (stat.isSymbolicLink() || !stat.isFile() || stat.uid !== uid || (stat.mode & 0o022) !== 0) throw new Error(label + ' is not a safe current-user-owned regular file'); if (executable && (stat.mode & 0o111) === 0) throw new Error(label + ' is not executable'); if (exactMode !== undefined && (stat.mode & 0o777) !== exactMode) throw new Error(label + ' has unsafe permissions'); return stat; }",
|
|
1846
|
+
"function smallDigest(filePath, label, uid, executable) { const stat = ownedFile(filePath, label, uid, executable); if (stat.size > MAX_SMALL_FILE_BYTES) throw new Error(label + ' exceeds the small-file limit'); return { sha256: createHash('sha256').update(readFileSync(filePath)).digest('hex'), bytes: stat.size }; }",
|
|
1847
|
+
"function sameDigest(actual, expected, label) { if (!expected || typeof expected.sha256 !== 'string' || !Number.isSafeInteger(expected.bytes) || actual.sha256 !== expected.sha256 || actual.bytes !== expected.bytes) throw new Error(label + ' postimage does not match the receipt'); }",
|
|
1848
|
+
"function main() { const uid = typeof process.getuid === 'function' ? process.getuid() : undefined; if (uid === undefined || uid === 0) throw new Error('root or non-POSIX execution is refused'); const release = path.resolve(process.argv[2] ?? ''); const releaseStat = lstatSync(release); if (releaseStat.isSymbolicLink() || !releaseStat.isDirectory() || releaseStat.uid !== uid || (releaseStat.mode & 0o777) !== 0o500) throw new Error('release directory is unsafe'); const receiptPath = safePath(release, 'receipt.json', 'receipt'); const receiptStat = ownedFile(receiptPath, 'receipt', uid, false, 0o600); if (receiptStat.size > 64 * 1024) throw new Error('receipt exceeds the safe size limit'); const receipt = JSON.parse(readFileSync(receiptPath, 'utf8')); if (receipt.schemaVersion !== 2 || receipt.pinDigest !== EXPECTED_PIN_DIGEST) throw new Error('receipt is stale or invalid'); if (!receipt.layout || receipt.layout.runfilesRelativePath !== 'agy_acp_server.runfiles' || receipt.layout.kernelExecutableRelativePath !== 'agy_acp_server' || receipt.layout.externalHarnessRelativePath !== 'localharness_external' || !receipt.postimages || !receipt.wrapper) throw new Error('receipt layout is invalid'); const runfiles = safePath(release, receipt.layout.runfilesRelativePath, 'runfiles'); const runfilesStat = lstatSync(runfiles); if (runfilesStat.isSymbolicLink() || !runfilesStat.isDirectory() || runfilesStat.uid !== uid || (runfilesStat.mode & 0o777) !== 0o500) throw new Error('runfiles directory is unsafe'); for (const name of ['modelSelection', 'proxyServer', 'serverControl', 'compatModule']) { const postimage = receipt.postimages[name]; if (!postimage || !safeRelative(postimage.relativePath)) throw new Error(name + ' receipt postimage is invalid'); sameDigest(smallDigest(safePath(runfiles, postimage.relativePath, name), name, uid, false), postimage, name); } const wrapper = safePath(release, receipt.wrapper.relativePath, 'wrapper'); sameDigest(smallDigest(wrapper, 'wrapper', uid, true), { sha256: receipt.wrapper.sha256, bytes: receipt.wrapper.bytes }, 'wrapper'); const verifier = safePath(release, receipt.wrapper.verifierRelativePath, 'wrapper verifier'); sameDigest(smallDigest(verifier, 'wrapper verifier', uid, true), { sha256: receipt.wrapper.verifierSha256, bytes: receipt.wrapper.verifierBytes }, 'wrapper verifier'); ownedFile(safePath(release, 'agy_acp_server', 'kernel executable'), 'kernel executable', uid, true, 0o500); ownedFile(safePath(release, 'localharness_external', 'external harness'), 'external harness', uid, true, 0o500); for (const [name, relativePath] of Object.entries(receipt.layout.importerEnvironment ?? {})) { if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !safeRelative(relativePath)) throw new Error('importer environment is invalid'); const importer = safePath(runfiles, relativePath, 'importer environment'); const importerStat = lstatSync(importer); if (importerStat.isSymbolicLink() || (!importerStat.isFile() && !importerStat.isDirectory()) || importerStat.uid !== uid) throw new Error('importer environment is unsafe'); } }",
|
|
1849
|
+
"try { main(); } catch (error) { fail(error instanceof Error ? error.message : 'verification failed'); }"
|
|
1850
|
+
].join("\n") + "\n";
|
|
1851
|
+
}
|
|
1852
|
+
function renderReleaseWrapper(layout) {
|
|
1853
|
+
const importerExports = Object.entries(layout.importerEnvironment)
|
|
1854
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1855
|
+
.map(([name, relativePath]) => `export ${name}=\"$release_dir/${RUNFILES_DIRECTORY}/${shellPath(relativePath)}\"`);
|
|
1856
|
+
return [
|
|
1857
|
+
"#!/usr/bin/env bash",
|
|
1858
|
+
"set -euo pipefail",
|
|
1859
|
+
"if [ \"$(id -u)\" -eq 0 ]; then",
|
|
1860
|
+
" printf '%s\\n' 'agy-acp kernel compatibility wrapper refuses root mode' >&2",
|
|
1861
|
+
" exit 1",
|
|
1862
|
+
"fi",
|
|
1863
|
+
"release_dir=\"$(cd -- \"$(dirname -- \"${BASH_SOURCE[0]}\")\" && pwd -P)\"",
|
|
1864
|
+
`node \"$release_dir/${RELEASE_VERIFIER_FILE}\" \"$release_dir\"`,
|
|
1865
|
+
`export ANTIGRAVITY_HARNESS_PATH=\"$release_dir/${EXTERNAL_HARNESS_FILE}\"`,
|
|
1866
|
+
"export PYTHONDONTWRITEBYTECODE=1",
|
|
1867
|
+
`export PYTHONPATH=\"$release_dir/${RUNFILES_DIRECTORY}\${PYTHONPATH:+:$PYTHONPATH}\"`,
|
|
1868
|
+
...importerExports,
|
|
1869
|
+
`exec \"$release_dir/${KERNEL_EXECUTABLE_FILE}\" \"--uid=\" \"$@\"`
|
|
1870
|
+
].join("\n") + "\n";
|
|
1871
|
+
}
|
|
1872
|
+
function renderStableWrapper() {
|
|
1873
|
+
return [
|
|
1874
|
+
"#!/usr/bin/env bash",
|
|
1875
|
+
"set -euo pipefail",
|
|
1876
|
+
"if [ \"$(id -u)\" -eq 0 ]; then",
|
|
1877
|
+
" printf '%s\\n' 'agy-acp active compatibility wrapper refuses root mode' >&2",
|
|
1878
|
+
" exit 1",
|
|
1879
|
+
"fi",
|
|
1880
|
+
"state_root=\"$(cd -- \"$(dirname -- \"${BASH_SOURCE[0]}\")\" && pwd -P)\"",
|
|
1881
|
+
`release_wrapper=\"$(node \"$state_root/${ACTIVE_RESOLVER_FILE}\" \"$state_root\")\"`,
|
|
1882
|
+
"exec \"$release_wrapper\" \"$@\""
|
|
1883
|
+
].join("\n") + "\n";
|
|
1884
|
+
}
|
|
1885
|
+
function renderActiveResolver(expectedPinDigest) {
|
|
1886
|
+
return [
|
|
1887
|
+
"#!/usr/bin/env node",
|
|
1888
|
+
"import { lstatSync, readFileSync, readlinkSync } from 'node:fs';",
|
|
1889
|
+
"import path from 'node:path';",
|
|
1890
|
+
`const EXPECTED_PIN_DIGEST = ${JSON.stringify(expectedPinDigest)};`,
|
|
1891
|
+
"function fail(message) { process.stderr.write('agy-acp active compatibility wrapper refused to run: ' + message + '\\n'); process.exit(1); }",
|
|
1892
|
+
"function identifier(value) { return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value); }",
|
|
1893
|
+
"function ownedDirectory(filePath, label, uid, mode) { const stat = lstatSync(filePath); if (stat.isSymbolicLink() || !stat.isDirectory() || stat.uid !== uid || (stat.mode & 0o777) !== mode) throw new Error(label + ' is unsafe'); }",
|
|
1894
|
+
"function ownedFile(filePath, label, uid, mode, executable) { const stat = lstatSync(filePath); if (stat.isSymbolicLink() || !stat.isFile() || stat.uid !== uid || (stat.mode & 0o777) !== mode || (stat.mode & 0o022) !== 0 || (executable && (stat.mode & 0o111) === 0)) throw new Error(label + ' is unsafe'); }",
|
|
1895
|
+
"function safeChild(root, name, label) { if (!identifier(name)) throw new Error(label + ' is invalid'); const result = path.resolve(root, name); if (path.dirname(result) !== path.resolve(root)) throw new Error(label + ' escapes the state root'); return result; }",
|
|
1896
|
+
"function main() { const uid = typeof process.getuid === 'function' ? process.getuid() : undefined; if (uid === undefined || uid === 0) throw new Error('root or non-POSIX execution is refused'); const root = path.resolve(process.argv[2] ?? ''); ownedDirectory(root, 'state root', uid, 0o700); const active = path.join(root, 'active'); const activeStat = lstatSync(active); if (!activeStat.isSymbolicLink() || activeStat.uid !== uid) throw new Error('active state reference is unsafe'); const target = readlinkSync(active); const prefix = 'activation-states/'; if (path.isAbsolute(target) || !target.startsWith(prefix) || target.slice(prefix.length).includes('/') || !identifier(target.slice(prefix.length))) throw new Error('active state reference escapes managed activation states'); const states = path.join(root, 'activation-states'); ownedDirectory(states, 'activation states directory', uid, 0o700); const stateDirectory = safeChild(states, target.slice(prefix.length), 'activation state'); ownedDirectory(stateDirectory, 'activation state', uid, 0o500); const statePath = path.join(stateDirectory, 'activation.json'); ownedFile(statePath, 'activation state', uid, 0o400, false); const stateStat = lstatSync(statePath); if (stateStat.size > 16 * 1024) throw new Error('activation state exceeds the safe size limit'); const state = JSON.parse(readFileSync(statePath, 'utf8')); if (!state || state.schemaVersion !== 1 || state.pinDigest !== EXPECTED_PIN_DIGEST || !identifier(state.currentArtifactId) || (state.previousArtifactId !== undefined && !identifier(state.previousArtifactId))) throw new Error('activation state is stale or invalid'); const releases = path.join(root, 'releases'); ownedDirectory(releases, 'releases directory', uid, 0o700); const release = safeChild(releases, state.currentArtifactId, 'current release'); ownedDirectory(release, 'current release', uid, 0o500); const wrapper = path.join(release, 'agy-acp-kernel-compat'); ownedFile(wrapper, 'current release wrapper', uid, 0o500, true); process.stdout.write(wrapper + '\\n'); }",
|
|
1897
|
+
"try { main(); } catch (error) { fail(error instanceof Error ? error.message : 'resolution failed'); }"
|
|
1898
|
+
].join("\n") + "\n";
|
|
1899
|
+
}
|
|
1900
|
+
function shellPath(relativePath) {
|
|
1901
|
+
assertSafeRelativePath("wrapper path", relativePath);
|
|
1902
|
+
return relativePath;
|
|
1903
|
+
}
|
|
1904
|
+
//# sourceMappingURL=kernel-compat-lifecycle.js.map
|