@openclaw/openshell-sandbox 2026.10.1-beta.2 → 2026.10.5-beta.1
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/dist/index.js +69 -111
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +91 -0
- package/node_modules/@openclaw/fs-safe/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +38 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +195 -0
- package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
- package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
- package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +52 -6
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +35 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +18 -18
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
- package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +5 -1
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +109 -1
- package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
- package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
- package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
- package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
- package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
- package/node_modules/@openclaw/fs-safe/docs/watch.md +6 -3
- package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
- package/node_modules/@openclaw/fs-safe/package.json +16 -16
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
|
@@ -6,6 +6,7 @@ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
|
6
6
|
import { hasNodeErrorCode } from "./path.js";
|
|
7
7
|
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
8
8
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
|
|
9
|
+
import { privateTempDirectoryFix, secureTempDirectoryFailure } from "./temp-directory-diagnostics.js";
|
|
9
10
|
import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
|
|
10
11
|
export function resolveSecureTempRoot(options) {
|
|
11
12
|
const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
|
|
@@ -82,8 +83,17 @@ export function resolveSecureTempRoot(options) {
|
|
|
82
83
|
candidate = lstatSync(candidatePath);
|
|
83
84
|
}
|
|
84
85
|
catch (error) {
|
|
85
|
-
return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error
|
|
86
|
+
return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error,
|
|
87
|
+
detail: `directory metadata could not be read; check path traversal/access permissions; ${privateTempDirectoryFix(uid)}` };
|
|
86
88
|
}
|
|
89
|
+
const failureDetail = () => {
|
|
90
|
+
try {
|
|
91
|
+
return secureTempDirectoryFailure(candidate, uid, windows);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return `directory security facts could not be read; ${privateTempDirectoryFix(uid)}`;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
87
97
|
let receipt;
|
|
88
98
|
try {
|
|
89
99
|
if (descriptor && uid !== undefined) {
|
|
@@ -91,26 +101,28 @@ export function resolveSecureTempRoot(options) {
|
|
|
91
101
|
}
|
|
92
102
|
if (receipt ? (receipt.mode & 18n) !== 0n :
|
|
93
103
|
candidate.isDirectory() !== true || candidate.isSymbolicLink() !== false || !isSecureDirForUser(candidate)) {
|
|
94
|
-
return { kind: "invalid", receipt };
|
|
104
|
+
return { kind: "invalid", receipt, detail: failureDetail() };
|
|
95
105
|
}
|
|
96
106
|
accessSync(candidatePath, TMP_DIR_ACCESS_MODE);
|
|
97
107
|
return { kind: "available", receipt };
|
|
98
108
|
}
|
|
99
109
|
catch (error) {
|
|
100
|
-
return { kind: "invalid", receipt, error };
|
|
110
|
+
return { kind: "invalid", receipt, error, detail: failureDetail() };
|
|
101
111
|
}
|
|
102
112
|
};
|
|
103
113
|
const tryRepair = (candidatePath, state, finalize = false) => {
|
|
104
114
|
assertNoWindowsPathAlias(candidatePath, "filesystem", "temp directory uses a Windows filesystem namespace alias", admissionPlatform);
|
|
105
115
|
if (!descriptor || uid === undefined || !state.receipt) {
|
|
106
|
-
return finalize ? { kind: "invalid", error: new Error("Secure temp descriptor finalization is unavailable.")
|
|
116
|
+
return finalize ? { ...state, kind: "invalid", error: new Error("Secure temp descriptor finalization is unavailable."),
|
|
117
|
+
detail: `descriptor finalization is unavailable; ${privateTempDirectoryFix(uid)}` } : state;
|
|
107
118
|
}
|
|
108
119
|
try {
|
|
109
120
|
repairSecureTempDirectory(candidatePath, state.receipt, uid, descriptor, () => accessSync(candidatePath, TMP_DIR_ACCESS_MODE), () => warn(`${warningPrefix} tightened permissions on temp dir: ${candidatePath}`), finalize);
|
|
110
121
|
return { kind: "available" };
|
|
111
122
|
}
|
|
112
123
|
catch (error) {
|
|
113
|
-
return { kind: "invalid", error
|
|
124
|
+
return { kind: "invalid", error,
|
|
125
|
+
detail: state.detail ?? `descriptor identity, permissions, or access could not be verified during repair; ${privateTempDirectoryFix(uid)}` };
|
|
114
126
|
}
|
|
115
127
|
};
|
|
116
128
|
const ensureTrustedFallbackDir = () => {
|
|
@@ -128,7 +140,7 @@ export function resolveSecureTempRoot(options) {
|
|
|
128
140
|
finalize = !windows && (suppliedMkdir !== undefined || created !== undefined);
|
|
129
141
|
}
|
|
130
142
|
catch (cause) {
|
|
131
|
-
throw new Error(`Unable to create fallback ${unsafeFallbackLabel}: ${fallbackPath}`, { cause });
|
|
143
|
+
throw new Error(`Unable to create fallback ${unsafeFallbackLabel}: ${JSON.stringify(fallbackPath)}; check parent directory write/search access; ${privateTempDirectoryFix(uid)}`, { cause });
|
|
132
144
|
}
|
|
133
145
|
// A concurrent mkdir winner has no implicit ownership or mode authority.
|
|
134
146
|
state = resolveDirState(entryPath);
|
|
@@ -137,7 +149,7 @@ export function resolveSecureTempRoot(options) {
|
|
|
137
149
|
}
|
|
138
150
|
state = tryRepair(entryPath, state, finalize);
|
|
139
151
|
if (state.kind !== "available") {
|
|
140
|
-
throw new Error(`Unsafe fallback ${unsafeFallbackLabel}: ${fallbackPath}`, { cause: state.error });
|
|
152
|
+
throw new Error(`Unsafe fallback ${unsafeFallbackLabel}: ${JSON.stringify(fallbackPath)}; ${state.detail}`, { cause: state.error });
|
|
141
153
|
}
|
|
142
154
|
return fallbackPath;
|
|
143
155
|
};
|
|
@@ -8,6 +8,8 @@ import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
|
8
8
|
import { isWindowsReservedDeviceName } from "./device-path.js";
|
|
9
9
|
import { FsSafeError } from "./errors.js";
|
|
10
10
|
import { getNativeBinding } from "./native.js";
|
|
11
|
+
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
12
|
+
import { cachedNoReplaceDevice, isNoReplaceUnavailable } from "./native-noreplace.js";
|
|
11
13
|
import { handoffPrivateProducerFile, } from "./private-producer-handoff.js";
|
|
12
14
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
13
15
|
import { rootFromDirectoryGuard } from "./root-impl.js";
|
|
@@ -109,8 +111,18 @@ async function writeIsolatedProducer(params) {
|
|
|
109
111
|
const result = await Reflect.apply(write, writeReceiver, [producerPath]);
|
|
110
112
|
assertCurrent();
|
|
111
113
|
if (native) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
try {
|
|
115
|
+
const cached = cachedNoReplaceDevice(native, parentGuard.stat.dev, "producer handoff");
|
|
116
|
+
if (cached)
|
|
117
|
+
throw cached;
|
|
118
|
+
await targetRoot.move(path.relative(targetRoot.rootReal, producerPath), path.basename(tempPath), { assertBeforeMutation: assertCurrent });
|
|
119
|
+
return { cleanupWorkspace, result };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (isFsSafeNativeRequired() || !isNoReplaceUnavailable(error))
|
|
123
|
+
throw error;
|
|
124
|
+
assertCurrent();
|
|
125
|
+
}
|
|
114
126
|
}
|
|
115
127
|
const handoff = await handoffPrivateProducerFile({
|
|
116
128
|
sourcePath: producerPath,
|
|
@@ -126,7 +138,10 @@ async function writeIsolatedProducer(params) {
|
|
|
126
138
|
await cleanupWorkspace();
|
|
127
139
|
}
|
|
128
140
|
catch (cleanupError) {
|
|
129
|
-
|
|
141
|
+
const combined = new AggregateError([error, cleanupError], "isolated producer operation and workspace cleanup failed");
|
|
142
|
+
if (isNoReplaceUnavailable(error))
|
|
143
|
+
throw new FsSafeError(error.code, error.message, { cause: combined });
|
|
144
|
+
throw combined;
|
|
130
145
|
}
|
|
131
146
|
throw error;
|
|
132
147
|
}
|
|
@@ -2,6 +2,7 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { fileObservation } from "./file-observation.js";
|
|
5
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
5
6
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
6
7
|
import { openSidecarRoot } from "./sidecar-lock-root.js";
|
|
7
8
|
import { createNativeExclusiveFile } from "./native-operations.js";
|
|
@@ -14,7 +15,8 @@ import { ancestryHasSidecarAdmission, captureSidecarAdmissionAncestry, createSid
|
|
|
14
15
|
import { resolveSidecarLockPaths } from "./sidecar-lock-target.js";
|
|
15
16
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
16
17
|
import { sleep } from "./timing.js";
|
|
17
|
-
import {
|
|
18
|
+
import { sidecarRetainedCreate } from "./root-create-input.js";
|
|
19
|
+
import { cleanupCreatedSidecar, retainCreatedSidecar } from "./sidecar-lock-retained.js";
|
|
18
20
|
import { stopSidecarLockMonitoring } from "./sidecar-lock-handle.js";
|
|
19
21
|
export async function acquireSidecarLock(options, context) {
|
|
20
22
|
const admissionAncestry = captureSidecarAdmissionAncestry();
|
|
@@ -166,6 +168,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
166
168
|
continue;
|
|
167
169
|
}
|
|
168
170
|
let handle = null;
|
|
171
|
+
let retainedSidecar;
|
|
169
172
|
let createdSnapshot = null, createdHeld;
|
|
170
173
|
let admissionConflict = false;
|
|
171
174
|
let exclusiveCreateConflict = false;
|
|
@@ -247,7 +250,13 @@ export async function acquireSidecarLock(options, context) {
|
|
|
247
250
|
const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
|
|
248
251
|
const observation = fileObservation();
|
|
249
252
|
try {
|
|
250
|
-
await observation.run(() => lockRoot.create(relativeLockPath, raw, {
|
|
253
|
+
await observation.run(() => lockRoot.create(relativeLockPath, raw, {
|
|
254
|
+
mkdir: true, mode: 0o600,
|
|
255
|
+
...sidecarRetainedCreate({
|
|
256
|
+
retain(fd, parent, nativeParent) { retainedSidecar = retainCreatedSidecar(lockPath, raw, fd, parent, nativeParent); },
|
|
257
|
+
cleanup(fd, parent, identity) { cleanupCreatedSidecar(lockPath, raw, fd, parent, identity); },
|
|
258
|
+
}),
|
|
259
|
+
}));
|
|
251
260
|
}
|
|
252
261
|
catch (error) {
|
|
253
262
|
// Only this invocation's failed exclusive open grants denial retry authority.
|
|
@@ -262,6 +271,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
262
271
|
createdSnapshot = { raw, payload, ownershipToken };
|
|
263
272
|
const opened = await openSidecarRoot(lockRoot, relativeLockPath, "unlinked");
|
|
264
273
|
if (!opened) {
|
|
274
|
+
retainedSidecar?.settle(true, false);
|
|
265
275
|
await waitForRetry();
|
|
266
276
|
continue;
|
|
267
277
|
}
|
|
@@ -299,7 +309,11 @@ export async function acquireSidecarLock(options, context) {
|
|
|
299
309
|
}
|
|
300
310
|
const snapshot = { raw, payload, stat: fsSync.fstatSync(handle.fd, { bigint: true }), ownershipToken };
|
|
301
311
|
createdSnapshot = snapshot;
|
|
312
|
+
if (retainedSidecar && !sameFileIdentityForCleanup(retainedSidecar.identity, snapshot.stat)) {
|
|
313
|
+
throw new FsSafeError("path-mismatch", "created sidecar lock identity changed before admission");
|
|
314
|
+
}
|
|
302
315
|
if (snapshot.stat.nlink === 0n) {
|
|
316
|
+
retainedSidecar?.settle(true, false);
|
|
303
317
|
await handle.close();
|
|
304
318
|
handle = null;
|
|
305
319
|
await waitForRetry();
|
|
@@ -310,10 +324,15 @@ export async function acquireSidecarLock(options, context) {
|
|
|
310
324
|
admissionConflict = true;
|
|
311
325
|
throw Object.assign(new Error("sidecar lock admission changed"), { code: "EEXIST" });
|
|
312
326
|
}
|
|
327
|
+
// An open descendant file prevents Windows directory relocation. The
|
|
328
|
+
// native receipt retains its parent and verifies the leaf at settlement.
|
|
329
|
+
if (retainedSidecar && process.platform === "win32")
|
|
330
|
+
await handle.close();
|
|
313
331
|
createdHeld = {
|
|
314
332
|
refCount: 1,
|
|
315
333
|
reentrantOwner: requestedReentrantOwner,
|
|
316
334
|
handle,
|
|
335
|
+
retainedSidecar,
|
|
317
336
|
lockPath,
|
|
318
337
|
snapshot,
|
|
319
338
|
acquiredAt: Date.now(),
|
|
@@ -380,7 +399,10 @@ export async function acquireSidecarLock(options, context) {
|
|
|
380
399
|
await handle.close().catch(() => undefined);
|
|
381
400
|
}
|
|
382
401
|
// Root records use the creator receipt; partial raw writes use fd identity.
|
|
383
|
-
if (
|
|
402
|
+
if (retainedSidecar) {
|
|
403
|
+
retainedSidecar.settle(true, false);
|
|
404
|
+
}
|
|
405
|
+
else if (failedSnapshot) {
|
|
384
406
|
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
385
407
|
lockRoot,
|
|
386
408
|
parsePayload: conditionalSidecarLockParser(parserState, admission, () => admission.hasToken() && !context.held.has(normalizedTargetPath)),
|
|
@@ -2,10 +2,12 @@ import type { NativeFileHandle } from "./native-operations.js";
|
|
|
2
2
|
import type { Root } from "./root-impl.js";
|
|
3
3
|
import type { SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
4
4
|
import type { SidecarLockHandle } from "./sidecar-lock-types.js";
|
|
5
|
+
import type { RetainedSidecar } from "./sidecar-lock-retained.js";
|
|
5
6
|
export type HeldSidecarLock = {
|
|
6
7
|
refCount: number;
|
|
7
8
|
reentrantOwner?: string;
|
|
8
9
|
handle: NativeFileHandle;
|
|
10
|
+
retainedSidecar?: RetainedSidecar;
|
|
9
11
|
lockPath: string;
|
|
10
12
|
snapshot: SidecarLockSnapshot;
|
|
11
13
|
acquiredAt: number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
|
+
import type { NativeWriteParent, PublishedWriteIdentity } from "./pinned-write-types.js";
|
|
3
|
+
export interface RetainedSidecar {
|
|
4
|
+
readonly identity: Readonly<{
|
|
5
|
+
dev: bigint;
|
|
6
|
+
ino: bigint;
|
|
7
|
+
}>;
|
|
8
|
+
/** Synchronous settlement also works in the process exit handler. Always closes. */
|
|
9
|
+
settle(remove: boolean, reportMismatch?: boolean): void;
|
|
10
|
+
}
|
|
11
|
+
/** Borrowed creation authority: never reopens a path or closes the writer's fds. */
|
|
12
|
+
export declare function cleanupCreatedSidecar(lockPath: string, raw: string, fd: number, parent: NativeWriteParent, identity: PublishedWriteIdentity): void;
|
|
13
|
+
/** Called only by the exclusive creator while its verified descriptor is alive. */
|
|
14
|
+
export declare function retainCreatedSidecar(lockPath: string, raw: string, createdFd: number, guard: AnyAsyncDirectoryGuard, nativeParent?: NativeWriteParent): RetainedSidecar | undefined;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { hasErrorCode } from "./file-cleanup.js";
|
|
5
|
+
import { captureNativeFdClose } from "./native-binding.js";
|
|
6
|
+
import { nativeOpenFlags } from "./native-operations.js";
|
|
7
|
+
import { sameFileIdentityForCleanup, sha256Hex } from "./file-identity.js";
|
|
8
|
+
function mismatch(check) {
|
|
9
|
+
return new FsSafeError("path-mismatch", `created sidecar lock ${check} changed; replacement preserved`);
|
|
10
|
+
}
|
|
11
|
+
function assertOwnerRecord(fd, expected) {
|
|
12
|
+
if (fs.fstatSync(fd, { bigint: true }).size !== BigInt(expected.length))
|
|
13
|
+
throw mismatch("owner record");
|
|
14
|
+
const bytes = Buffer.alloc(expected.length);
|
|
15
|
+
let offset = 0;
|
|
16
|
+
while (offset < bytes.length) {
|
|
17
|
+
const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
18
|
+
if (read === 0)
|
|
19
|
+
throw mismatch("owner record");
|
|
20
|
+
offset += read;
|
|
21
|
+
}
|
|
22
|
+
if (!bytes.equals(expected))
|
|
23
|
+
throw mismatch("owner record");
|
|
24
|
+
}
|
|
25
|
+
/** Borrowed creation authority: never reopens a path or closes the writer's fds. */
|
|
26
|
+
export function cleanupCreatedSidecar(lockPath, raw, fd, parent, identity) {
|
|
27
|
+
const name = path.basename(lockPath);
|
|
28
|
+
const windows = process.platform === "win32";
|
|
29
|
+
try {
|
|
30
|
+
if (windows) {
|
|
31
|
+
if (!parent.binding.rootRemovalStat || !parent.binding.rootRemovalUnlink) {
|
|
32
|
+
throw new FsSafeError("helper-unavailable", "creator cleanup is unavailable");
|
|
33
|
+
}
|
|
34
|
+
const entry = parent.binding.rootRemovalStat(parent.fd, name);
|
|
35
|
+
if (entry.directory || entry.symlink || !sameFileIdentityForCleanup(identity, entry))
|
|
36
|
+
throw mismatch("identity");
|
|
37
|
+
}
|
|
38
|
+
else if (!parent.binding.stagedFileMatches || !parent.binding.removeStagedFile) {
|
|
39
|
+
throw new FsSafeError("helper-unavailable", "creator cleanup is unavailable");
|
|
40
|
+
}
|
|
41
|
+
else if (!parent.binding.stagedFileMatches(parent.fd, name, fd)) {
|
|
42
|
+
throw mismatch("identity");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
47
|
+
return;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
51
|
+
if (!stat.isFile() || stat.nlink !== 1n || !sameFileIdentityForCleanup(identity, stat))
|
|
52
|
+
throw mismatch("identity");
|
|
53
|
+
assertOwnerRecord(fd, Buffer.from(raw));
|
|
54
|
+
if (windows) {
|
|
55
|
+
try {
|
|
56
|
+
parent.binding.rootRemovalUnlink(parent.fd, name, identity.dev, identity.ino, false);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (!hasErrorCode(error, "ENOENT"))
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
if (parent.binding.removeStagedFile(parent.fd, name, fd) === "preserved")
|
|
65
|
+
throw mismatch("identity");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Called only by the exclusive creator while its verified descriptor is alive. */
|
|
69
|
+
export function retainCreatedSidecar(lockPath, raw, createdFd, guard, nativeParent) {
|
|
70
|
+
const binding = nativeParent?.binding;
|
|
71
|
+
if (!binding)
|
|
72
|
+
return undefined;
|
|
73
|
+
const created = fs.fstatSync(createdFd, { bigint: true });
|
|
74
|
+
if (!created.isFile() || created.nlink !== 1n)
|
|
75
|
+
throw mismatch("identity");
|
|
76
|
+
const identity = Object.freeze({ dev: created.dev, ino: created.ino });
|
|
77
|
+
const name = path.basename(lockPath);
|
|
78
|
+
if (process.platform === "win32") {
|
|
79
|
+
if (!binding.retainWindowsSidecar)
|
|
80
|
+
return undefined;
|
|
81
|
+
const native = binding.retainWindowsSidecar(nativeParent.fd, guard.realPath, name, BigInt(guard.stat.dev), BigInt(guard.stat.ino), created.dev, created.ino, BigInt(Buffer.byteLength(raw)), sha256Hex(raw), 1024 * 1024);
|
|
82
|
+
const admitted = native.admission;
|
|
83
|
+
if (admitted.status === "unsupported" && admitted.resources === "closed")
|
|
84
|
+
return undefined;
|
|
85
|
+
if (admitted.status !== "retained") {
|
|
86
|
+
throw new FsSafeError("helper-failed", "could not retain created sidecar lock", { details: { result: admitted } });
|
|
87
|
+
}
|
|
88
|
+
return { identity, settle(remove, reportMismatch = true) {
|
|
89
|
+
const result = native.settle(remove);
|
|
90
|
+
if (result.status === "preserved-mismatch") {
|
|
91
|
+
if (reportMismatch || result.resources !== "closed") {
|
|
92
|
+
throw mismatch(result.errors.some(error => /digest|size changed/u.test(error.message)) ? "owner record" : "identity");
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (result.errors.length || result.resources !== "closed" ||
|
|
97
|
+
(remove && result.disposition !== "accepted" && result.namespace !== "absent")) {
|
|
98
|
+
throw new FsSafeError("helper-failed", "retained sidecar lock settlement failed", { details: { result } });
|
|
99
|
+
}
|
|
100
|
+
} };
|
|
101
|
+
}
|
|
102
|
+
if (!binding.stagedFileMatches || !binding.removeStagedFile)
|
|
103
|
+
return undefined;
|
|
104
|
+
const closeFile = captureNativeFdClose(binding);
|
|
105
|
+
const parentFd = binding.openBeneath(nativeParent.fd, "", nativeOpenFlags(fs.constants.O_RDONLY | fs.constants.O_DIRECTORY)).fd;
|
|
106
|
+
let fd;
|
|
107
|
+
try {
|
|
108
|
+
const parentStat = fs.fstatSync(parentFd, { bigint: true });
|
|
109
|
+
if (!parentStat.isDirectory() || !sameFileIdentityForCleanup(guard.stat, parentStat))
|
|
110
|
+
throw mismatch("identity");
|
|
111
|
+
fd = binding.openBeneath(parentFd, name, nativeOpenFlags(fs.constants.O_RDONLY | fs.constants.O_NONBLOCK)).fd;
|
|
112
|
+
const ownedFd = fd;
|
|
113
|
+
const assertIdentity = () => {
|
|
114
|
+
try {
|
|
115
|
+
if (!binding.stagedFileMatches(parentFd, name, ownedFd))
|
|
116
|
+
throw mismatch("identity");
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
120
|
+
return false;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const current = fs.fstatSync(ownedFd, { bigint: true });
|
|
124
|
+
if (!current.isFile() || current.nlink !== 1n || !sameFileIdentityForCleanup(created, current))
|
|
125
|
+
throw mismatch("identity");
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
const expected = Buffer.from(raw);
|
|
129
|
+
const assertOwner = () => assertOwnerRecord(ownedFd, expected);
|
|
130
|
+
if (!assertIdentity())
|
|
131
|
+
throw mismatch("identity");
|
|
132
|
+
assertOwner();
|
|
133
|
+
let settled = false;
|
|
134
|
+
return { identity, settle(remove, reportMismatch = true) {
|
|
135
|
+
if (settled)
|
|
136
|
+
return;
|
|
137
|
+
settled = true;
|
|
138
|
+
const errors = [];
|
|
139
|
+
try {
|
|
140
|
+
if (remove && assertIdentity()) {
|
|
141
|
+
assertOwner();
|
|
142
|
+
if (assertIdentity() && binding.removeStagedFile(parentFd, name, ownedFd) === "preserved")
|
|
143
|
+
throw mismatch("identity");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (reportMismatch || !(error instanceof FsSafeError && error.code === "path-mismatch"))
|
|
148
|
+
errors.push(error);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
closeFile(ownedFd);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
errors.push(error);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
closeFile(parentFd);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
errors.push(error);
|
|
161
|
+
}
|
|
162
|
+
if (errors.length === 1)
|
|
163
|
+
throw errors[0];
|
|
164
|
+
if (errors.length)
|
|
165
|
+
throw new AggregateError(errors, "sidecar lock cleanup and close failed");
|
|
166
|
+
} };
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const errors = [error];
|
|
170
|
+
try {
|
|
171
|
+
if (fd !== undefined)
|
|
172
|
+
closeFile(fd);
|
|
173
|
+
}
|
|
174
|
+
catch (closeError) {
|
|
175
|
+
errors.push(closeError);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
closeFile(parentFd);
|
|
179
|
+
}
|
|
180
|
+
catch (closeError) {
|
|
181
|
+
errors.push(closeError);
|
|
182
|
+
}
|
|
183
|
+
if (errors.length > 1)
|
|
184
|
+
throw new AggregateError(errors, "sidecar retention and close failed");
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -108,7 +108,10 @@ function releaseAllLocksSync(state, options) {
|
|
|
108
108
|
void held.handle.close().catch(() => undefined);
|
|
109
109
|
try {
|
|
110
110
|
const retained = options?.preserveRetained === true && held.retainOnExit;
|
|
111
|
-
if (
|
|
111
|
+
if (held.retainedSidecar) {
|
|
112
|
+
held.retainedSidecar.settle(!retained);
|
|
113
|
+
}
|
|
114
|
+
else if (!retained && !held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
|
|
112
115
|
fsSync.rmSync(held.lockPath, { force: true });
|
|
113
116
|
}
|
|
114
117
|
}
|
|
@@ -186,6 +189,17 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
|
|
|
186
189
|
}
|
|
187
190
|
held.releasePromise = (async () => {
|
|
188
191
|
await held.handle.close().catch(() => undefined);
|
|
192
|
+
if (held.retainedSidecar) {
|
|
193
|
+
try {
|
|
194
|
+
held.retainedSidecar.settle(true);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
if (state.held.get(normalizedTargetPath) === held)
|
|
198
|
+
state.held.delete(normalizedTargetPath);
|
|
199
|
+
stopSidecarLockMonitoring(held);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
189
203
|
await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot, {
|
|
190
204
|
lockRoot: held.lockRoot,
|
|
191
205
|
parsePayload: held.parsePayload,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { noReplaceUnavailable } from "./native-noreplace.js";
|
|
2
3
|
export function createStagedFileReceipt(directory, temporaryBasename, stat) {
|
|
3
4
|
return Object.freeze({
|
|
4
5
|
directory,
|
|
@@ -11,6 +12,7 @@ export function createStagedFileReceipt(directory, temporaryBasename, stat) {
|
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
export function stagedFailure(kind, error, details) {
|
|
15
|
+
error = noReplaceUnavailable(error, `${kind} publication`) ?? error;
|
|
14
16
|
let code = "helper-failed";
|
|
15
17
|
try {
|
|
16
18
|
code = error instanceof FsSafeError ? error.code
|
|
@@ -19,7 +21,8 @@ export function stagedFailure(kind, error, details) {
|
|
|
19
21
|
catch {
|
|
20
22
|
// Uninspectable error metadata must not interrupt terminal settlement.
|
|
21
23
|
}
|
|
22
|
-
return new FsSafeError(code,
|
|
24
|
+
return new FsSafeError(code, code === "helper-unavailable" && error instanceof FsSafeError
|
|
25
|
+
? error.message : `staged ${kind} ${details.phase} failed`, { cause: error, details });
|
|
23
26
|
}
|
|
24
27
|
/** Finish owned cleanup and every close before surfacing publication evidence. */
|
|
25
28
|
export async function settleStagedFile(params) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function tempDirectoryMode(mode: number | bigint): string;
|
|
2
|
+
export declare function privateTempDirectoryFix(uid: number | undefined): string;
|
|
3
|
+
type TempDirectoryStat = {
|
|
4
|
+
isDirectory(): boolean;
|
|
5
|
+
isSymbolicLink(): boolean;
|
|
6
|
+
uid?: number | bigint;
|
|
7
|
+
mode?: number | bigint;
|
|
8
|
+
};
|
|
9
|
+
export declare function secureTempDirectoryFailure(stat: TempDirectoryStat, uid: number | undefined, windows: boolean): string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function tempDirectoryMode(mode) {
|
|
2
|
+
return (typeof mode === "bigint" ? mode & 4095n : mode & 0o7777).toString(8).padStart(4, "0");
|
|
3
|
+
}
|
|
4
|
+
export function privateTempDirectoryFix(uid) {
|
|
5
|
+
return `use a real private directory${uid === undefined ? "" : ` owned by uid ${uid}`} with mode 0700 under trusted ancestors (or use resolveSecureTempRoot()); do not chmod a shared temp directory to 0700`;
|
|
6
|
+
}
|
|
7
|
+
// Called only after failed admission; diagnostics never authorize repair.
|
|
8
|
+
export function secureTempDirectoryFailure(stat, uid, windows) {
|
|
9
|
+
if (stat.isSymbolicLink())
|
|
10
|
+
return "is a symbolic link; choose a real directory, not a symlink";
|
|
11
|
+
if (!stat.isDirectory())
|
|
12
|
+
return "is not a directory; choose a real directory";
|
|
13
|
+
if (!windows && uid !== undefined) {
|
|
14
|
+
if (stat.uid !== undefined && stat.uid !== uid && stat.uid !== BigInt(uid)) {
|
|
15
|
+
return `has owner uid ${stat.uid}, expected uid ${uid}; ${privateTempDirectoryFix(uid)}`;
|
|
16
|
+
}
|
|
17
|
+
const mode = stat.mode;
|
|
18
|
+
if (mode !== undefined) {
|
|
19
|
+
if ((typeof mode === "number" && (!Number.isSafeInteger(mode) || mode < 0)) ||
|
|
20
|
+
(typeof mode === "bigint" && mode < 0n)) {
|
|
21
|
+
return `has invalid mode bits; ${privateTempDirectoryFix(uid)}`;
|
|
22
|
+
}
|
|
23
|
+
if (typeof mode === "bigint" ? (mode & 18n) !== 0n : (mode & 0o022) !== 0) {
|
|
24
|
+
return `has group/world-writable mode ${tempDirectoryMode(mode)} (a sticky bit does not make a private root safe); ${privateTempDirectoryFix(uid)}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return `directory identity, permissions, or write/search access could not be verified; ${privateTempDirectoryFix(uid)}`;
|
|
29
|
+
}
|
|
@@ -143,7 +143,7 @@ export async function createOwnedTempFile(params) {
|
|
|
143
143
|
capability.assertCurrent();
|
|
144
144
|
const initial = inspectDirectoryIdentitySync(dir);
|
|
145
145
|
identity = Object.freeze({ dev: initial.dev, ino: initial.ino });
|
|
146
|
-
const needsModeInitialization = validateInitialTempWorkspaceChild(initial, admission.ownerUid, 0o700);
|
|
146
|
+
const needsModeInitialization = validateInitialTempWorkspaceChild(initial, admission.ownerUid, 0o700, dir);
|
|
147
147
|
retainedChild = TempWorkspaceRetainedChild.retain(dir, identity);
|
|
148
148
|
if (needsModeInitialization) {
|
|
149
149
|
await retainedChild.initializeMode(0o700, admission.ownerUid, admission.assertCurrent);
|
|
@@ -46,7 +46,7 @@ function snapshotFromExactObservation(dir, uid, realPath, stat, privateDirectory
|
|
|
46
46
|
if (stat.symbolicLink || !stat.directory) {
|
|
47
47
|
throw new FsSafeError("not-file", "temp workspace root component must be a real directory");
|
|
48
48
|
}
|
|
49
|
-
assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory);
|
|
49
|
+
assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory, dir);
|
|
50
50
|
const identity = Object.freeze({ dev: stat.dev, ino: stat.ino });
|
|
51
51
|
// Retain copied immutable scalars, never a mutable Stats object supplied by
|
|
52
52
|
// an observation hook. Every later permission check uses a fresh snapshot.
|
|
@@ -106,7 +106,7 @@ function inspectSnapshotIdentity(entry) {
|
|
|
106
106
|
}
|
|
107
107
|
function assertSnapshot(entry, uid) {
|
|
108
108
|
const stat = inspectSnapshotIdentity(entry);
|
|
109
|
-
assertTrustedTempWorkspaceDirectory(stat, uid, entry.privateDirectory);
|
|
109
|
+
assertTrustedTempWorkspaceDirectory(stat, uid, entry.privateDirectory, entry.dir);
|
|
110
110
|
assertCanonicalRoot(entry);
|
|
111
111
|
}
|
|
112
112
|
function snapshotMissingRootComponent(parent, dir, ownerUid, privateDirectory) {
|
|
@@ -115,7 +115,7 @@ function snapshotMissingRootComponent(parent, dir, ownerUid, privateDirectory) {
|
|
|
115
115
|
return snapshot(dir, ownerUid, undefined, privateDirectory);
|
|
116
116
|
}
|
|
117
117
|
const current = inspectSnapshotIdentity(parent);
|
|
118
|
-
assertTrustedTempWorkspaceDirectory(current, ownerUid, parent.privateDirectory);
|
|
118
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid, parent.privateDirectory, parent.dir);
|
|
119
119
|
let realPath;
|
|
120
120
|
try {
|
|
121
121
|
realPath = canonicalTempWorkspacePath(dir);
|
|
@@ -136,7 +136,7 @@ function snapshotMissingRootComponent(parent, dir, ownerUid, privateDirectory) {
|
|
|
136
136
|
function assertChain(chain, uid) {
|
|
137
137
|
for (const entry of chain) {
|
|
138
138
|
const current = inspectSnapshotIdentity(entry);
|
|
139
|
-
assertTrustedTempWorkspaceDirectory(current, uid, entry.privateDirectory);
|
|
139
|
+
assertTrustedTempWorkspaceDirectory(current, uid, entry.privateDirectory, entry.dir);
|
|
140
140
|
}
|
|
141
141
|
const last = chain[chain.length - 1];
|
|
142
142
|
assertCanonicalRoot(last);
|
|
@@ -159,7 +159,7 @@ function associateTempWorkspaceRoot(entry, ownerUid, descriptorFd) {
|
|
|
159
159
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
160
160
|
throw new FsSafeError("not-file", "temp workspace cleanup parent must be a real directory");
|
|
161
161
|
}
|
|
162
|
-
assertTrustedTempWorkspaceDirectory(stat, ownerUid, entry.privateDirectory);
|
|
162
|
+
assertTrustedTempWorkspaceDirectory(stat, ownerUid, entry.privateDirectory, entry.dir);
|
|
163
163
|
}
|
|
164
164
|
function discoverMissingTempWorkspaceAncestor(ancestor, missing, missingError) {
|
|
165
165
|
// The caller already classified the first ENOENT. Keep this handoff out of
|
|
@@ -290,7 +290,7 @@ function canonicalRootAdmission(discovery, ownerUid) {
|
|
|
290
290
|
return snapshot(dir, ownerUid, dir, index === ancestry.length - 1 && discovery.privateDirectory).entry;
|
|
291
291
|
}
|
|
292
292
|
const current = inspectSnapshotIdentity(discovery);
|
|
293
|
-
assertTrustedTempWorkspaceDirectory(current, ownerUid, discovery.privateDirectory);
|
|
293
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid, discovery.privateDirectory, discovery.dir);
|
|
294
294
|
return discovery;
|
|
295
295
|
});
|
|
296
296
|
const current = candidate[candidate.length - 1];
|
|
@@ -14,10 +14,10 @@ export declare function projectTempWorkspaceNumericIdentity(identity: TempWorksp
|
|
|
14
14
|
export declare function inspectTempWorkspaceDescriptorIdentitySync(fd: number, expected: TempWorkspaceIdentity, numeric: TempWorkspaceNumericIdentity | undefined): TempWorkspaceIdentityStat;
|
|
15
15
|
export declare function inspectTempWorkspaceDirectoryIdentitySync(dir: string, expected: TempWorkspaceIdentity, numeric: TempWorkspaceNumericIdentity | undefined): TempWorkspaceIdentityStat;
|
|
16
16
|
export declare function validateTempWorkspaceDirMode(mode: number): void;
|
|
17
|
-
export declare function assertTrustedTempWorkspaceDirectory(stat: Pick<BigIntStats, "uid" | "gid" | "mode"> | Pick<Stats, "uid" | "gid" | "mode">, uid: number | undefined, privateDirectory
|
|
18
|
-
export declare function assertTempWorkspaceChildState(stat: BigIntStats | Stats, ownerUid: number | undefined): void;
|
|
19
|
-
export declare function validateInitialTempWorkspaceChild(stat: BigIntStats, ownerUid: number | undefined, mode: number): boolean;
|
|
17
|
+
export declare function assertTrustedTempWorkspaceDirectory(stat: Pick<BigIntStats, "uid" | "gid" | "mode"> | Pick<Stats, "uid" | "gid" | "mode">, uid: number | undefined, privateDirectory: boolean, dir: string): void;
|
|
18
|
+
export declare function assertTempWorkspaceChildState(stat: BigIntStats | Stats, ownerUid: number | undefined, dir: string): void;
|
|
19
|
+
export declare function validateInitialTempWorkspaceChild(stat: BigIntStats, ownerUid: number | undefined, mode: number, dir: string): boolean;
|
|
20
20
|
export declare function childHasRequestedMode(stat: BigIntStats | Stats, mode: number): boolean;
|
|
21
21
|
export declare function admitTempWorkspaceChild(dir: string, expected: BigIntStats, parent: TempWorkspaceRootAdmission, mode: number): Promise<void> | undefined;
|
|
22
22
|
export declare function admitTempWorkspaceChildSync(dir: string, expected: BigIntStats, parent: TempWorkspaceRootAdmission, mode: number): void;
|
|
23
|
-
export declare function validateAdmittedTempWorkspaceChild(current: TempWorkspaceIdentityStat, ownerUid: number | undefined, mode: number): void;
|
|
23
|
+
export declare function validateAdmittedTempWorkspaceChild(current: TempWorkspaceIdentityStat, ownerUid: number | undefined, mode: number, dir: string): void;
|