@openclaw/openshell-sandbox 2026.10.5-beta.4 → 2026.10.10-beta.2
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/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
- package/node_modules/@openclaw/fs-safe/README.md +20 -4
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
- package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
- package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
- package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
- package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
- package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
- package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
- package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
- 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 +15 -5
- package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
- package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
- package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
- package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
- package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
- package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
- package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
- package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
- package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
- package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
- package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
- package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
- package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
- package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
- package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
- package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
- package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
- package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
- package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
- package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
- package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
- package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
- package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
- package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
- package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
- package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
- package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
- package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
- package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
- package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
- package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
- package/node_modules/@openclaw/fs-safe/package.json +21 -12
- 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-freebsd-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
- 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-arm64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
- 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/windows-security-bridge.cs +0 -336
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
|
@@ -266,7 +266,9 @@ export async function removePathInRootFallback(root, targetPath, options, receip
|
|
|
266
266
|
const rootGuard = await createAsyncDirectoryGuard(root.rootReal, { bigint: true });
|
|
267
267
|
if (!sameFileIdentityForCleanup(rootGuard.stat, root.rootIdentity))
|
|
268
268
|
throw rootPathChangedError();
|
|
269
|
-
const guards = [
|
|
269
|
+
const guards = [
|
|
270
|
+
createRemovalDirectoryAssertion(rootGuard.dir, rootGuard.stat, rootGuard.realPath),
|
|
271
|
+
];
|
|
270
272
|
let ancestor = root.rootReal;
|
|
271
273
|
const parentRelative = path.relative(root.rootReal, path.dirname(targetPath));
|
|
272
274
|
for (const segment of parentRelative.split(path.sep).filter(Boolean)) {
|
|
@@ -287,12 +289,12 @@ export async function removePathInRootFallback(root, targetPath, options, receip
|
|
|
287
289
|
if (!isPathInside(guard.realPath, ancestor) || !isPathInside(ancestor, guard.realPath)) {
|
|
288
290
|
throw new FsSafeError("path-mismatch", "removal ancestor changed during operation");
|
|
289
291
|
}
|
|
290
|
-
guards.push(guard);
|
|
292
|
+
guards.push(createRemovalDirectoryAssertion(guard.dir, guard.stat, guard.realPath));
|
|
291
293
|
}
|
|
292
294
|
function assertCurrent() {
|
|
293
295
|
for (const guard of guards) {
|
|
294
296
|
try {
|
|
295
|
-
|
|
297
|
+
assertRemovalDirectoryCurrent(guard);
|
|
296
298
|
}
|
|
297
299
|
catch (error) {
|
|
298
300
|
// force tolerates a vanished leaf, never a lost traversal boundary.
|
|
@@ -342,7 +344,7 @@ export async function removePathInRootFallback(root, targetPath, options, receip
|
|
|
342
344
|
!sameFileIdentityForCleanup(directoryGuard.stat, initial)) {
|
|
343
345
|
throw new FsSafeError("path-mismatch", "removal directory changed during operation");
|
|
344
346
|
}
|
|
345
|
-
guards.push(directoryGuard);
|
|
347
|
+
guards.push(createRemovalDirectoryAssertion(directoryGuard.dir, directoryGuard.stat, directoryGuard.realPath));
|
|
346
348
|
try {
|
|
347
349
|
assertCurrent();
|
|
348
350
|
assertNotAborted(options.signal);
|
|
@@ -29,23 +29,9 @@ function entryKind(entry) {
|
|
|
29
29
|
function limitEntry(relativePath) {
|
|
30
30
|
return { relativePath, kind: "truncated", size: 0 };
|
|
31
31
|
}
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
if (options.order !== undefined && !["sorted", "filesystem"].includes(options.order)) {
|
|
37
|
-
throw new TypeError(`invalid root walk order: ${String(options.order)}`);
|
|
38
|
-
}
|
|
39
|
-
if (options.limitBehavior !== undefined &&
|
|
40
|
-
!["truncate", "throw"].includes(options.limitBehavior)) {
|
|
41
|
-
throw new TypeError(`invalid root walk limit behavior: ${String(options.limitBehavior)}`);
|
|
42
|
-
}
|
|
43
|
-
if (options.onDirectoryError !== undefined &&
|
|
44
|
-
!["throw", "skip-and-report"].includes(options.onDirectoryError)) {
|
|
45
|
-
throw new TypeError(`invalid root walk directory error behavior: ${String(options.onDirectoryError)}`);
|
|
46
|
-
}
|
|
47
|
-
const maxDepth = validateBudget("maxDepth", options.maxDepth);
|
|
48
|
-
const maxEntries = validateBudget("maxEntries", options.maxEntries);
|
|
32
|
+
export function walkRoot(root, relativePath, options) {
|
|
33
|
+
let maxDepth;
|
|
34
|
+
let maxEntries;
|
|
49
35
|
const visitedDirectories = new Set();
|
|
50
36
|
let examined = 0;
|
|
51
37
|
let truncated = false;
|
|
@@ -69,6 +55,24 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
69
55
|
return { relativePath: directory, kind: "directory-error", size: 0, error };
|
|
70
56
|
};
|
|
71
57
|
async function* visit(directory, depth, admittedChildPath) {
|
|
58
|
+
if (depth === 0) {
|
|
59
|
+
if (!["skip", "follow-within-root", "include"].includes(options.symlinkPolicy)) {
|
|
60
|
+
throw new TypeError(`invalid root walk symlink policy: ${String(options.symlinkPolicy)}`);
|
|
61
|
+
}
|
|
62
|
+
if (options.order !== undefined && !["sorted", "filesystem"].includes(options.order)) {
|
|
63
|
+
throw new TypeError(`invalid root walk order: ${String(options.order)}`);
|
|
64
|
+
}
|
|
65
|
+
if (options.limitBehavior !== undefined &&
|
|
66
|
+
!["truncate", "throw"].includes(options.limitBehavior)) {
|
|
67
|
+
throw new TypeError(`invalid root walk limit behavior: ${String(options.limitBehavior)}`);
|
|
68
|
+
}
|
|
69
|
+
if (options.onDirectoryError !== undefined &&
|
|
70
|
+
!["throw", "skip-and-report"].includes(options.onDirectoryError)) {
|
|
71
|
+
throw new TypeError(`invalid root walk directory error behavior: ${String(options.onDirectoryError)}`);
|
|
72
|
+
}
|
|
73
|
+
maxDepth = validateBudget("maxDepth", options.maxDepth);
|
|
74
|
+
maxEntries = validateBudget("maxEntries", options.maxEntries);
|
|
75
|
+
}
|
|
72
76
|
options.signal?.throwIfAborted();
|
|
73
77
|
let listing;
|
|
74
78
|
let canonicalDirectory;
|
|
@@ -142,7 +146,7 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
142
146
|
while (true) {
|
|
143
147
|
let next;
|
|
144
148
|
try {
|
|
145
|
-
next = await listing.next();
|
|
149
|
+
next = listing.nextSnapshot ? listing.nextSnapshot() : await listing.next();
|
|
146
150
|
options.signal?.throwIfAborted();
|
|
147
151
|
}
|
|
148
152
|
catch (error) {
|
|
@@ -228,5 +232,5 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
228
232
|
}
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
|
-
|
|
235
|
+
return visit(relativePath, 0);
|
|
232
236
|
}
|
|
@@ -260,12 +260,14 @@ export async function resolvePinnedWriteTargetInRoot(root, relativePath, request
|
|
|
260
260
|
}
|
|
261
261
|
if (!overwrite) {
|
|
262
262
|
try {
|
|
263
|
-
const existing = fsSync.statSync(resolved);
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
263
|
+
const existing = fsSync.statSync(resolved, { throwIfNoEntry: false });
|
|
264
|
+
if (existing) {
|
|
265
|
+
if (!existing.isFile() && !existing.isDirectory())
|
|
266
|
+
throw new FsSafeError("not-file", "not a file");
|
|
267
|
+
if (existing.isFile() && existing.nlink > 1)
|
|
268
|
+
throw hardlinkedPathNotAllowedError();
|
|
269
|
+
throw new FsSafeError("already-exists", "file already exists");
|
|
270
|
+
}
|
|
269
271
|
}
|
|
270
272
|
catch (error) {
|
|
271
273
|
if (!isNotFoundPathError(error))
|
|
@@ -62,7 +62,11 @@ export async function finishRootFallbackWrite(params) {
|
|
|
62
62
|
// Final mode belongs to the accepted object, including on rename-unstable
|
|
63
63
|
// filesystems; never chmod the old temp and assume its mode was transferred.
|
|
64
64
|
try {
|
|
65
|
-
|
|
65
|
+
// Windows chmod only changes the owner-write bit (the read-only attribute).
|
|
66
|
+
const modeChange = process.platform !== "win32" ||
|
|
67
|
+
(fsSync.fstatSync(handle.fd).mode & 0o200) !== (params.mode & 0o200);
|
|
68
|
+
if (modeChange)
|
|
69
|
+
fsSync.fchmodSync(handle.fd, params.mode);
|
|
66
70
|
}
|
|
67
71
|
catch (error) {
|
|
68
72
|
await cleanupPinnedFilePath({
|
|
@@ -3,6 +3,7 @@ import { getNativeBinding } from "./native.js";
|
|
|
3
3
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
4
4
|
import { warnNativeFallback } from "./native-fallback-warning.js";
|
|
5
5
|
import { inspectWindowsDescriptorCommand } from "./windows-security-command.js";
|
|
6
|
+
import { assertWindowsHelperAllowed } from "./windows-appcontainer.js";
|
|
6
7
|
import { unverified as permissionUnverified, validateSecureWindowsSecurityFacts } from "./windows-security-facts.js";
|
|
7
8
|
const IDENTITY_RE = /^([0-9a-f]{8}):([0-9a-f]{16})$/;
|
|
8
9
|
const TRUSTED_OWNER_CLASSES = new Set(["current-user", "system", "administrators"]);
|
|
@@ -61,7 +62,8 @@ export async function inspectSecureWindowsFile(params) {
|
|
|
61
62
|
permissionUnverified("Windows descriptor ACL verification requires an up-to-date native helper");
|
|
62
63
|
}
|
|
63
64
|
if (!nativeAvailable) {
|
|
64
|
-
|
|
65
|
+
assertWindowsHelperAllowed();
|
|
66
|
+
warnNativeFallback("windows-secure-file", "Windows descriptor ACL inspection launches the packaged .NET helper for each check.", "inspectWindowsSecureFileHandle");
|
|
65
67
|
}
|
|
66
68
|
let result;
|
|
67
69
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExactFileIdentity } from "./exact-identity.js";
|
|
1
2
|
import { type Stats } from "node:fs";
|
|
2
3
|
import { type PermissionCheck, type PermissionCheckOptions } from "./permissions.js";
|
|
3
4
|
export type SecureFileReadOptions = {
|
|
@@ -26,6 +27,7 @@ export type SecureFileReadResult = {
|
|
|
26
27
|
buffer: Buffer;
|
|
27
28
|
realPath: string;
|
|
28
29
|
stat: Stats;
|
|
30
|
+
exactIdentity: ExactFileIdentity;
|
|
29
31
|
permissions?: PermissionCheck;
|
|
30
32
|
};
|
|
31
33
|
export declare function readSecureFile(options: SecureFileReadOptions): Promise<SecureFileReadResult>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { exactFileIdentity, numberStats } from "./exact-identity.js";
|
|
1
2
|
import fsSync, {} from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -135,11 +136,16 @@ async function openSecureHandle(options, maxBytes) {
|
|
|
135
136
|
throw err;
|
|
136
137
|
}
|
|
137
138
|
try {
|
|
138
|
-
const
|
|
139
|
+
const openedIdentity = inspectFileIdentitySync(() => {
|
|
140
|
+
const observed = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
141
|
+
if (!observed.isFile())
|
|
142
|
+
throw new FsSafeError("not-file", `${label(options)} must be a file: ${options.filePath}`);
|
|
143
|
+
return observed;
|
|
144
|
+
});
|
|
145
|
+
const openedStat = numberStats(openedIdentity);
|
|
139
146
|
if (!openedStat.isFile()) {
|
|
140
147
|
throw new FsSafeError("not-file", `${label(options)} must be a file: ${options.filePath}`);
|
|
141
148
|
}
|
|
142
|
-
const openedIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(handle.fd, { bigint: true }));
|
|
143
149
|
assertNotHardlinked(options, openedIdentity);
|
|
144
150
|
inspectFileIdentitySync(() => {
|
|
145
151
|
const pathStat = options.trust?.allowSymlink
|
|
@@ -255,7 +261,7 @@ async function assertSecurePermissions(options, opened, fd) {
|
|
|
255
261
|
return permissions;
|
|
256
262
|
}
|
|
257
263
|
async function readHandleWithTimeout(handle, timeoutMs, maxBytes) {
|
|
258
|
-
const read = () =>
|
|
264
|
+
const read = () => readFileHandleBounded(handle, maxBytes ?? Infinity);
|
|
259
265
|
if (timeoutMs === undefined || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
260
266
|
return await read();
|
|
261
267
|
}
|
|
@@ -290,7 +296,7 @@ export async function readSecureFile(options) {
|
|
|
290
296
|
throw new FsSafeError("not-file", `${label(options)} must remain a file: ${options.filePath}`);
|
|
291
297
|
}
|
|
292
298
|
assertNotHardlinked(options, finalIdentity);
|
|
293
|
-
return { buffer, realPath: opened.realPath, stat: opened.pathStat, permissions };
|
|
299
|
+
return { buffer, realPath: opened.realPath, stat: opened.pathStat, exactIdentity: exactFileIdentity(opened.identity), permissions };
|
|
294
300
|
}
|
|
295
301
|
finally {
|
|
296
302
|
await opened.handle.close().catch(() => undefined);
|
|
@@ -75,7 +75,7 @@ async function writeIsolatedProducer(params) {
|
|
|
75
75
|
const targetRoot = rootFromDirectoryGuard(parentGuard);
|
|
76
76
|
// Select the handoff before invoking the producer. Missing required native
|
|
77
77
|
// support must not leave producer output behind as a discovery side effect.
|
|
78
|
-
const native = getNativeBinding();
|
|
78
|
+
const native = getNativeBinding("openBeneath", "renameNoReplace");
|
|
79
79
|
const cleanupErrors = [];
|
|
80
80
|
const { target, identity } = await createOwnedTempFile({
|
|
81
81
|
rootDir: targetRoot.rootReal,
|
|
@@ -246,8 +246,8 @@ export async function sidecarReclaimGuardExists(pathname, lockRoot) {
|
|
|
246
246
|
try {
|
|
247
247
|
if (lockRoot)
|
|
248
248
|
await lockRoot.stat(relativeSidecarLockPath(lockRoot, pathname));
|
|
249
|
-
else
|
|
250
|
-
|
|
249
|
+
else if (fsSync.lstatSync(pathname, { throwIfNoEntry: false }) === undefined)
|
|
250
|
+
return false;
|
|
251
251
|
return true;
|
|
252
252
|
}
|
|
253
253
|
catch (error) {
|
|
@@ -99,7 +99,7 @@ export function retainCreatedSidecar(lockPath, raw, createdFd, guard, nativePare
|
|
|
99
99
|
}
|
|
100
100
|
} };
|
|
101
101
|
}
|
|
102
|
-
if (!binding.stagedFileMatches || !binding.removeStagedFile)
|
|
102
|
+
if (!binding.openBeneath || !binding.stagedFileMatches || !binding.removeStagedFile)
|
|
103
103
|
return undefined;
|
|
104
104
|
const closeFile = captureNativeFdClose(binding);
|
|
105
105
|
const parentFd = binding.openBeneath(nativeParent.fd, "", nativeOpenFlags(fs.constants.O_RDONLY | fs.constants.O_DIRECTORY)).fd;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
@@ -23,7 +24,13 @@ export async function resolveSidecarTargetPath(resolved, lockRoot) {
|
|
|
23
24
|
assertNoWindowsPathAlias(normalized);
|
|
24
25
|
return normalized;
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
let parentExists = false;
|
|
28
|
+
try {
|
|
29
|
+
parentExists = fsSync.statSync(dir).isDirectory();
|
|
30
|
+
}
|
|
31
|
+
catch { /* Preserve mkdir errors below. */ }
|
|
32
|
+
if (!parentExists)
|
|
33
|
+
await fs.mkdir(recursiveMkdirPath(dir), { recursive: true });
|
|
27
34
|
let parent;
|
|
28
35
|
try {
|
|
29
36
|
parent = realpathSync.native(dir);
|
|
@@ -18,6 +18,19 @@ export declare function exactIdentityMatches(expected: FileIdentityStat, actual:
|
|
|
18
18
|
}>): boolean;
|
|
19
19
|
export declare function describeStagedDirectory(fd: number, pathname: string): StagedDirectorySnapshot;
|
|
20
20
|
export declare function assertStagedDirectoryCurrent(receipt: StagedDirectorySnapshot): BigIntStats;
|
|
21
|
+
export declare function assertNativeStagedDirectoryCurrent(binding: NativeBinding, fd: number, directory: StagedDirectorySnapshot): void;
|
|
22
|
+
/** Ordinary publication fences identity, while sibling entries may change. */
|
|
23
|
+
export declare function describeNativeStagedDirectory(binding: NativeBinding, fd: number, canonicalPath: string): {
|
|
24
|
+
directory: Readonly<{
|
|
25
|
+
path: string;
|
|
26
|
+
realPath: string;
|
|
27
|
+
identity: Readonly<{
|
|
28
|
+
dev: bigint;
|
|
29
|
+
ino: bigint;
|
|
30
|
+
}>;
|
|
31
|
+
}>;
|
|
32
|
+
stat: BigIntStats;
|
|
33
|
+
};
|
|
21
34
|
export declare function describePolicyStagedDirectory(fd: number, pathname: string, binding?: NativeBinding): PolicyStagedDirectory;
|
|
22
35
|
export declare function assertPolicyStagedDirectoryCurrent(captured: PolicyStagedDirectory): BigIntStats;
|
|
23
36
|
export declare function refreshPolicyStagedDirectoryObservation(captured: PolicyStagedDirectory): MutationDirectoryObservation;
|
|
@@ -34,6 +34,35 @@ export function assertStagedDirectoryCurrent(receipt) {
|
|
|
34
34
|
}
|
|
35
35
|
return current;
|
|
36
36
|
}
|
|
37
|
+
export function assertNativeStagedDirectoryCurrent(binding, fd, directory) {
|
|
38
|
+
if (typeof binding.stagedDirectoryMatches === "function" && directory.path === directory.realPath) {
|
|
39
|
+
try {
|
|
40
|
+
if (!binding.stagedDirectoryMatches(fd, directory.path, directory.identity.dev, directory.identity.ino)) {
|
|
41
|
+
throw new FsSafeError("path-mismatch", "staging directory pathname changed");
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error?.code !== "OBSERVATION_UNAVAILABLE")
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
assertStagedDirectoryCurrent(directory);
|
|
51
|
+
}
|
|
52
|
+
/** Ordinary publication fences identity, while sibling entries may change. */
|
|
53
|
+
export function describeNativeStagedDirectory(binding, fd, canonicalPath) {
|
|
54
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
55
|
+
if (!stat.isDirectory())
|
|
56
|
+
throw new FsSafeError("not-file", "staging parent must be a directory");
|
|
57
|
+
const pathname = resolvePathPreservingWindowsRoot(canonicalPath);
|
|
58
|
+
const directory = Object.freeze({
|
|
59
|
+
path: pathname,
|
|
60
|
+
realPath: pathname,
|
|
61
|
+
identity: Object.freeze({ dev: stat.dev, ino: stat.ino }),
|
|
62
|
+
});
|
|
63
|
+
assertNativeStagedDirectoryCurrent(binding, fd, directory);
|
|
64
|
+
return { directory, stat };
|
|
65
|
+
}
|
|
37
66
|
function sameDirectoryMetadata(left, right) {
|
|
38
67
|
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
|
|
39
68
|
left.nlink === right.nlink;
|
|
@@ -3,7 +3,7 @@ import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync } from "./di
|
|
|
3
3
|
import { pinNodeDirectoryForMode, pinNodeDirectoryForModeSync } from "./directory-mode-node.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
-
import { classifyTempWorkspaceOwner, warnUnmappedTempWorkspaceAncestor } from "./temp-workspace-ownership.js";
|
|
6
|
+
import { classifyTempWorkspaceOwner, hasTrustedAndroidTempAncestorGroup, warnUnmappedTempWorkspaceAncestor } from "./temp-workspace-ownership.js";
|
|
7
7
|
import { privateTempDirectoryFix, tempDirectoryMode } from "./temp-directory-diagnostics.js";
|
|
8
8
|
export const TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY = process.platform === "linux" || process.platform === "darwin";
|
|
9
9
|
export function projectTempWorkspaceNumericIdentity(identity) {
|
|
@@ -54,9 +54,11 @@ export function assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory,
|
|
|
54
54
|
// Unmapped host owners are unverifiable; trust the host directory hierarchy.
|
|
55
55
|
// Admit sticky ancestors so PrivateUsers keeps host /tmp and PrivateTmp usable.
|
|
56
56
|
// Leaf roots stay euid-owned/private; non-sticky writable ancestors are rejected.
|
|
57
|
+
const writeMask = !privateDirectory && hasTrustedAndroidTempAncestorGroup(stat, uid, ownership)
|
|
58
|
+
? 0o002 : 0o022;
|
|
57
59
|
const writable = typeof stat.mode === "bigint"
|
|
58
|
-
? (stat.mode &
|
|
59
|
-
: Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode &
|
|
60
|
+
? (stat.mode & BigInt(writeMask)) !== 0n
|
|
61
|
+
: Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode & writeMask) !== 0;
|
|
60
62
|
const sticky = typeof stat.mode === "bigint"
|
|
61
63
|
? (stat.mode & 512n) !== 0n
|
|
62
64
|
: Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode & 0o1000) !== 0;
|
|
@@ -23,6 +23,10 @@ export declare class TempWorkspaceRetainedChild {
|
|
|
23
23
|
retained: TempWorkspaceRetainedChild;
|
|
24
24
|
stat: fsSync.BigIntStats;
|
|
25
25
|
};
|
|
26
|
+
static adoptCreated(dir: string, fd: number, searchOnly: boolean): {
|
|
27
|
+
retained: TempWorkspaceRetainedChild;
|
|
28
|
+
stat: fsSync.BigIntStats;
|
|
29
|
+
};
|
|
26
30
|
discardInitialReceipt(): void;
|
|
27
31
|
finalizeAdmission(ownerUid: number | undefined, mode: number): TempWorkspaceIdentityStat;
|
|
28
32
|
initializeMode(mode: number, ownerUid: number | undefined, assertParent: () => void): Promise<void>;
|
|
@@ -36,6 +40,10 @@ export declare class TempWorkspaceRetainedChild {
|
|
|
36
40
|
ino: bigint;
|
|
37
41
|
}>;
|
|
38
42
|
directory: RetainedChildDirectory | undefined;
|
|
43
|
+
creationActivity: Readonly<{
|
|
44
|
+
mtimeNs: bigint;
|
|
45
|
+
ctimeNs: bigint;
|
|
46
|
+
}> | undefined;
|
|
39
47
|
};
|
|
40
48
|
close(): void;
|
|
41
49
|
}
|
|
@@ -90,6 +90,7 @@ export class TempWorkspaceRetainedChild {
|
|
|
90
90
|
#dir;
|
|
91
91
|
#identity;
|
|
92
92
|
#numericIdentity;
|
|
93
|
+
#creationActivity;
|
|
93
94
|
#access;
|
|
94
95
|
#fd;
|
|
95
96
|
#modeLease = false;
|
|
@@ -102,6 +103,8 @@ export class TempWorkspaceRetainedChild {
|
|
|
102
103
|
}
|
|
103
104
|
this.#dir = dir;
|
|
104
105
|
this.#identity = Object.freeze({ dev: identity.dev, ino: identity.ino });
|
|
106
|
+
this.#creationActivity = typeof identity.mtimeNs === "bigint" && typeof identity.ctimeNs === "bigint"
|
|
107
|
+
? Object.freeze({ mtimeNs: identity.mtimeNs, ctimeNs: identity.ctimeNs }) : undefined;
|
|
105
108
|
this.#numericIdentity = TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY
|
|
106
109
|
? projectTempWorkspaceNumericIdentity(this.#identity)
|
|
107
110
|
: undefined;
|
|
@@ -127,6 +130,14 @@ export class TempWorkspaceRetainedChild {
|
|
|
127
130
|
}
|
|
128
131
|
static retainCreated(dir) {
|
|
129
132
|
const descriptor = openRetainedDirectory(dir);
|
|
133
|
+
return TempWorkspaceRetainedChild.#adoptCreated(dir, descriptor);
|
|
134
|
+
}
|
|
135
|
+
static adoptCreated(dir, fd, searchOnly) {
|
|
136
|
+
return TempWorkspaceRetainedChild.#adoptCreated(dir, {
|
|
137
|
+
fd, access: searchOnly ? "search" : "read", proc: searchOnly && process.platform === "linux",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
static #adoptCreated(dir, descriptor) {
|
|
130
141
|
try {
|
|
131
142
|
// Only this immediate post-create route installs a receipt. Freezing the
|
|
132
143
|
// exact fd result keeps its identity and complete mode immutable while
|
|
@@ -317,9 +328,9 @@ export class TempWorkspaceRetainedChild {
|
|
|
317
328
|
}
|
|
318
329
|
if (!retainDescriptor) {
|
|
319
330
|
fsSync.closeSync(fd);
|
|
320
|
-
return { dir: this.#dir, identity: this.#identity, directory: undefined };
|
|
331
|
+
return { dir: this.#dir, identity: this.#identity, directory: undefined, creationActivity: this.#creationActivity };
|
|
321
332
|
}
|
|
322
|
-
return { dir: this.#dir, identity: this.#identity, directory: { fd } };
|
|
333
|
+
return { dir: this.#dir, identity: this.#identity, directory: { fd }, creationActivity: this.#creationActivity };
|
|
323
334
|
}
|
|
324
335
|
close() {
|
|
325
336
|
this.#assertModeLeaseReleased();
|
|
@@ -191,6 +191,7 @@ export class TempWorkspaceCleanupOwner {
|
|
|
191
191
|
#identity;
|
|
192
192
|
#capability;
|
|
193
193
|
#directory;
|
|
194
|
+
#creationActivity;
|
|
194
195
|
#closed = false;
|
|
195
196
|
#running = false;
|
|
196
197
|
#exitInterrupted = false;
|
|
@@ -202,6 +203,7 @@ export class TempWorkspaceCleanupOwner {
|
|
|
202
203
|
this.#identity = child.identity;
|
|
203
204
|
this.#capability = capability;
|
|
204
205
|
this.#directory = child.directory;
|
|
206
|
+
this.#creationActivity = child.creationActivity;
|
|
205
207
|
}
|
|
206
208
|
#repeat() {
|
|
207
209
|
return this.#result === "removed" ? "missing" : this.#result;
|
|
@@ -307,7 +309,12 @@ export class TempWorkspaceCleanupOwner {
|
|
|
307
309
|
!sameFileIdentityForCleanup(quarantined, this.#identity)) {
|
|
308
310
|
return "indeterminate";
|
|
309
311
|
}
|
|
310
|
-
|
|
312
|
+
// Timestamps are only a performance hint. Every identity fence still
|
|
313
|
+
// runs, and even an unchanged/coarse timestamp must pass OS empty removal.
|
|
314
|
+
const tryEmpty = this.#creationActivity !== undefined &&
|
|
315
|
+
current.mtimeNs === this.#creationActivity.mtimeNs &&
|
|
316
|
+
current.ctimeNs === this.#creationActivity.ctimeNs;
|
|
317
|
+
return { name, path: quarantinePath, nativeRemoval, tryEmpty };
|
|
311
318
|
}
|
|
312
319
|
catch (error) {
|
|
313
320
|
if (error instanceof FsSafeError && error.code === "helper-unavailable")
|
|
@@ -335,11 +342,23 @@ export class TempWorkspaceCleanupOwner {
|
|
|
335
342
|
}
|
|
336
343
|
return result.outcome === "removed" ? "removed" : "indeterminate";
|
|
337
344
|
}
|
|
345
|
+
#tryRemoveEmptyNative(quarantine) {
|
|
346
|
+
if (!quarantine.tryEmpty)
|
|
347
|
+
return undefined;
|
|
348
|
+
const binding = this.#capability.binding;
|
|
349
|
+
const result = binding.tryRemoveEmptyOwnedTree?.(this.#capability.parent.fd, quarantine.name, this.#directory.fd);
|
|
350
|
+
if (!result || (!result.errorCode && result.outcome === "not-empty"))
|
|
351
|
+
return undefined;
|
|
352
|
+
return this.#mapRemoval(result);
|
|
353
|
+
}
|
|
338
354
|
async #remove(quarantine) {
|
|
339
355
|
if (quarantine.nativeRemoval) {
|
|
340
356
|
const beforeNativeRemoval = getFsSafeTestHooks()?.beforeTempWorkspaceNativeRemoval;
|
|
341
357
|
if (beforeNativeRemoval)
|
|
342
358
|
await beforeNativeRemoval(quarantine.path);
|
|
359
|
+
const emptyResult = this.#tryRemoveEmptyNative(quarantine);
|
|
360
|
+
if (emptyResult !== undefined)
|
|
361
|
+
return emptyResult;
|
|
343
362
|
return this.#mapRemoval(await this.#capability.binding.removeOwnedTree(this.#capability.parent.fd, quarantine.name, this.#directory.fd));
|
|
344
363
|
}
|
|
345
364
|
try {
|
|
@@ -349,7 +368,31 @@ export class TempWorkspaceCleanupOwner {
|
|
|
349
368
|
return "indeterminate";
|
|
350
369
|
}
|
|
351
370
|
// Removal failures propagate unchanged; only authority uncertainty becomes a receipt.
|
|
352
|
-
|
|
371
|
+
const tryEmpty = quarantine.tryEmpty && process.platform !== "win32";
|
|
372
|
+
try {
|
|
373
|
+
if (tryEmpty)
|
|
374
|
+
await fs.rmdir(quarantine.path);
|
|
375
|
+
else
|
|
376
|
+
await fs.rm(quarantine.path, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (!tryEmpty)
|
|
380
|
+
throw error;
|
|
381
|
+
const code = error?.code;
|
|
382
|
+
if (code !== "ENOENT") {
|
|
383
|
+
if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "EPERM")
|
|
384
|
+
throw error;
|
|
385
|
+
// rmdir yielded before reporting a nonempty directory. Re-admit the
|
|
386
|
+
// quarantine before allowing pathname-recursive removal to follow it.
|
|
387
|
+
try {
|
|
388
|
+
this.#assertQuarantine(quarantine);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return "indeterminate";
|
|
392
|
+
}
|
|
393
|
+
await fs.rm(quarantine.path, { recursive: true, force: true });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
353
396
|
try {
|
|
354
397
|
this.#capability.assertCurrent();
|
|
355
398
|
return "removed";
|
|
@@ -361,6 +404,9 @@ export class TempWorkspaceCleanupOwner {
|
|
|
361
404
|
#removeSync(quarantine) {
|
|
362
405
|
if (quarantine.nativeRemoval) {
|
|
363
406
|
getFsSafeTestHooks()?.beforeTempWorkspaceNativeRemovalSync?.(quarantine.path);
|
|
407
|
+
const emptyResult = this.#tryRemoveEmptyNative(quarantine);
|
|
408
|
+
if (emptyResult !== undefined)
|
|
409
|
+
return emptyResult;
|
|
364
410
|
return this.#mapRemoval(this.#capability.binding.removeOwnedTreeSync(this.#capability.parent.fd, quarantine.name, this.#directory.fd));
|
|
365
411
|
}
|
|
366
412
|
try {
|
|
@@ -369,7 +415,29 @@ export class TempWorkspaceCleanupOwner {
|
|
|
369
415
|
catch {
|
|
370
416
|
return "indeterminate";
|
|
371
417
|
}
|
|
372
|
-
|
|
418
|
+
const tryEmpty = quarantine.tryEmpty && process.platform !== "win32";
|
|
419
|
+
try {
|
|
420
|
+
if (tryEmpty)
|
|
421
|
+
fsSync.rmdirSync(quarantine.path);
|
|
422
|
+
else
|
|
423
|
+
fsSync.rmSync(quarantine.path, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
if (!tryEmpty)
|
|
427
|
+
throw error;
|
|
428
|
+
const code = error?.code;
|
|
429
|
+
if (code !== "ENOENT") {
|
|
430
|
+
if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "EPERM")
|
|
431
|
+
throw error;
|
|
432
|
+
try {
|
|
433
|
+
this.#assertQuarantine(quarantine);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return "indeterminate";
|
|
437
|
+
}
|
|
438
|
+
fsSync.rmSync(quarantine.path, { recursive: true, force: true });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
373
441
|
try {
|
|
374
442
|
this.#capability.assertCurrent();
|
|
375
443
|
return "removed";
|
|
@@ -2,7 +2,8 @@ type DirectoryOwner = {
|
|
|
2
2
|
uid: number | bigint;
|
|
3
3
|
gid: number | bigint;
|
|
4
4
|
};
|
|
5
|
-
export type TempWorkspaceOwnership = "user" | "root" | "unmapped" | "foreign";
|
|
5
|
+
export type TempWorkspaceOwnership = "user" | "root" | "android-system" | "unmapped" | "foreign";
|
|
6
6
|
export declare function classifyTempWorkspaceOwner(stat: DirectoryOwner, uid: number): TempWorkspaceOwnership;
|
|
7
|
+
export declare function hasTrustedAndroidTempAncestorGroup(stat: DirectoryOwner, uid: number, ownership: TempWorkspaceOwnership): boolean;
|
|
7
8
|
export declare function warnUnmappedTempWorkspaceAncestor(): void;
|
|
8
9
|
export {};
|
|
@@ -28,6 +28,8 @@ export function classifyTempWorkspaceOwner(stat, uid) {
|
|
|
28
28
|
return "user";
|
|
29
29
|
if (stat.uid === 0 || stat.uid === 0n)
|
|
30
30
|
return "root";
|
|
31
|
+
if (process.platform === "android" && (stat.uid === 1000 || stat.uid === 1000n))
|
|
32
|
+
return "android-system";
|
|
31
33
|
if (process.platform !== "linux")
|
|
32
34
|
return "foreign";
|
|
33
35
|
try {
|
|
@@ -44,6 +46,26 @@ export function classifyTempWorkspaceOwner(stat, uid) {
|
|
|
44
46
|
return "foreign";
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
// Android reserves the system group and each app's matching UID/GID. This
|
|
50
|
+
// exception is for ancestor group-write only; private leaves still reject it.
|
|
51
|
+
export function hasTrustedAndroidTempAncestorGroup(stat, uid, ownership) {
|
|
52
|
+
if (process.platform !== "android")
|
|
53
|
+
return false;
|
|
54
|
+
if (ownership === "user" && uid % 100000 >= 10000 && uid % 100000 <= 19999) {
|
|
55
|
+
return stat.gid === uid || stat.gid === BigInt(uid);
|
|
56
|
+
}
|
|
57
|
+
if (ownership !== "android-system" || (stat.gid !== 1000 && stat.gid !== 1000n))
|
|
58
|
+
return false;
|
|
59
|
+
try {
|
|
60
|
+
const gid = process.getegid?.();
|
|
61
|
+
const groups = process.getgroups?.();
|
|
62
|
+
return gid !== undefined && Number.isSafeInteger(gid) && gid >= 0 && gid !== 1000 && Array.isArray(groups) &&
|
|
63
|
+
groups.every((group) => Number.isSafeInteger(group) && group >= 0 && group !== 1000);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
47
69
|
export function warnUnmappedTempWorkspaceAncestor() {
|
|
48
70
|
if (warned)
|
|
49
71
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExactFileIdentity } from "./exact-identity.js";
|
|
1
2
|
import type { FileStore, FileStoreSync } from "./file-store.js";
|
|
2
3
|
import type { TempPathIdentityReceipt } from "./temp-cleanup.js";
|
|
3
4
|
import type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety } from "./temp-workspace-owner.js";
|
|
@@ -11,6 +12,7 @@ export type TempWorkspaceOptions = {
|
|
|
11
12
|
export type TempWorkspace = {
|
|
12
13
|
dir: string;
|
|
13
14
|
identity: TempPathIdentityReceipt;
|
|
15
|
+
exactIdentity?: ExactFileIdentity;
|
|
14
16
|
store: FileStore;
|
|
15
17
|
path(fileName: string): string;
|
|
16
18
|
write(fileName: string, data: string | Uint8Array): Promise<string>;
|
|
@@ -26,6 +28,7 @@ export type TempWorkspace = {
|
|
|
26
28
|
export type TempWorkspaceSync = {
|
|
27
29
|
dir: string;
|
|
28
30
|
identity: TempPathIdentityReceipt;
|
|
31
|
+
exactIdentity?: ExactFileIdentity;
|
|
29
32
|
store: FileStoreSync;
|
|
30
33
|
path(fileName: string): string;
|
|
31
34
|
write(fileName: string, data: string | Uint8Array): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
|
+
export { holdWindowsSharingLock, setWindowsFileAttributes, readWindowsFileExtents } from "./windows-test-hooks.js";
|
|
3
4
|
export type FsSafeTestHooks = {
|
|
4
5
|
beforeWatchRegistration?: (path: string) => void | Promise<void>;
|
|
5
6
|
afterWatchRegistration?: (path: string) => void | Promise<void>;
|
|
@@ -97,7 +97,7 @@ function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
|
97
97
|
if (isForeignWindowsShareOrDevicePath(lexicalTarget, [...allowedRoots, ...resolvedRoots])) {
|
|
98
98
|
throw new Error(`Refusing to trash path outside allowed roots: ${targetPath}`);
|
|
99
99
|
}
|
|
100
|
-
const stat = fs.lstatSync(lexicalTarget);
|
|
100
|
+
const stat = fs.lstatSync(lexicalTarget, { bigint: true });
|
|
101
101
|
const resolvedTarget = resolveTrashTargetPath(targetPath);
|
|
102
102
|
const resolvedTargetPath = resolvedTarget.path;
|
|
103
103
|
const parent = createSyncDirectoryGuard(path.dirname(lexicalTarget));
|
|
@@ -121,7 +121,7 @@ function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
|
121
121
|
}
|
|
122
122
|
function assertTrashTargetGuard(guard) {
|
|
123
123
|
assertSyncDirectoryGuard(guard.parent);
|
|
124
|
-
const stat = fs.lstatSync(guard.path);
|
|
124
|
+
const stat = fs.lstatSync(guard.path, { bigint: true });
|
|
125
125
|
if (!sameFileIdentity(stat, guard.stat)) {
|
|
126
126
|
throw new Error(`Refusing to trash path after it changed: ${guard.path}`);
|
|
127
127
|
}
|