@zenfs/core 2.7.6 → 2.7.7
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/backends/store/fs.js +1 -3
- package/dist/backends/store/store.js +1 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +9 -3
- package/dist/internal/error.d.ts +5 -0
- package/dist/internal/error.js +10 -0
- package/dist/internal/inode.d.ts +25 -0
- package/dist/internal/inode.js +75 -10
- package/dist/mixins/async.js +22 -24
- package/dist/node/promises.js +17 -21
- package/dist/node/stats.d.ts +1 -1
- package/dist/node/stats.js +4 -1
- package/dist/node/sync.js +14 -18
- package/dist/polyfills.js +16 -0
- package/dist/vfs/async.js +77 -64
- package/dist/vfs/config.d.ts +10 -1
- package/dist/vfs/config.js +5 -2
- package/dist/vfs/file.js +10 -4
- package/dist/vfs/ioctl.js +2 -2
- package/dist/vfs/shared.d.ts +2 -0
- package/dist/vfs/shared.js +3 -4
- package/dist/vfs/sync.js +77 -60
- package/dist/vfs/vcache.d.ts +14 -14
- package/dist/vfs/vcache.js +65 -57
- package/dist/vfs/watchers.d.ts +1 -0
- package/dist/vfs/watchers.js +2 -4
- package/dist/vfs/xattr.js +18 -17
- package/package.json +2 -3
- package/scripts/make-index.js +1 -1
- package/tests/common/async.test.ts +35 -0
- package/tests/common/config.test.ts +1 -1
- package/tests/common/file_index.test.ts +1 -1
- package/tests/common/mounts.test.ts +12 -0
- package/tests/common/vcache.test.ts +13 -0
- package/tests/fs/concurrency.test.ts +3 -3
- package/tests/fs/links.test.ts +76 -0
- package/tests/fs/permissions.test.ts +232 -0
- package/tests/fs/stat.test.ts +1 -1
- package/tests/fs/streams.test.ts +1 -1
- package/tests/fs/xattr.test.ts +10 -0
package/scripts/make-index.js
CHANGED
|
@@ -94,7 +94,7 @@ function computeEntries(path) {
|
|
|
94
94
|
else if (!seenDevs.has(stats.dev))
|
|
95
95
|
if (options.xdev) {
|
|
96
96
|
console.warn(
|
|
97
|
-
`${styleText('yellowBright', `--xdev: Adding entries from device ${stats.dev} (${path}). You may get duplicate inos which MUST be de-duplicated manually.`)}
|
|
97
|
+
`${styleText('yellowBright', `--xdev: Adding entries from device ${stats.dev} (${path}). You may get duplicate inos which MUST be de-duplicated manually.`)}`,
|
|
98
98
|
);
|
|
99
99
|
seenDevs.add(stats.dev);
|
|
100
100
|
} else {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { Async, InMemory, InMemoryStore, StoreFS } from '@zenfs/core';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { suite, test } from 'node:test';
|
|
5
|
+
|
|
6
|
+
class AsyncMemory extends Async(StoreFS) {
|
|
7
|
+
_sync = InMemory.create({ label: 'sync' });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
suite('Async()', () => {
|
|
11
|
+
test('sync operations are applied to the sync cache once', async () => {
|
|
12
|
+
const fs = new AsyncMemory(new InMemoryStore());
|
|
13
|
+
fs.checkRootSync();
|
|
14
|
+
await fs.ready();
|
|
15
|
+
|
|
16
|
+
const calls: string[] = [];
|
|
17
|
+
for (const key of ['mkdirSync', 'rmdirSync', 'createFileSync', 'unlinkSync'] as const) {
|
|
18
|
+
const original = (fs._sync[key] as (...args: unknown[]) => unknown).bind(fs._sync);
|
|
19
|
+
(fs._sync as any)[key] = (...args: unknown[]) => {
|
|
20
|
+
calls.push(key);
|
|
21
|
+
return original(...args);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const options = { mode: 0o755, uid: 0, gid: 0 };
|
|
26
|
+
fs.mkdirSync('/dir', options);
|
|
27
|
+
fs.rmdirSync('/dir');
|
|
28
|
+
fs.createFileSync('/file', options);
|
|
29
|
+
fs.unlinkSync('/file');
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(calls, ['mkdirSync', 'rmdirSync', 'createFileSync', 'unlinkSync']);
|
|
33
|
+
assert.deepEqual(await fs.readdir('/'), []);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -19,6 +19,18 @@ suite('Mounts', () => {
|
|
|
19
19
|
fs.rmSync('/nested', { recursive: true, force: true });
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
test('lstat of a mount point is the mounted root #326', async () => {
|
|
23
|
+
await configure({ mounts: { '/mounted': InMemory } });
|
|
24
|
+
|
|
25
|
+
await fs.promises.utimes('/mounted', 1, 1);
|
|
26
|
+
assert.equal(fs.statSync('/mounted').mtimeMs, 1000);
|
|
27
|
+
assert.equal(fs.lstatSync('/mounted').mtimeMs, 1000);
|
|
28
|
+
assert.equal((await fs.promises.lstat('/mounted')).mtimeMs, 1000);
|
|
29
|
+
|
|
30
|
+
fs.umount('/mounted');
|
|
31
|
+
fs.rmdirSync('/mounted');
|
|
32
|
+
});
|
|
33
|
+
|
|
22
34
|
test('Race conditions', async () => {
|
|
23
35
|
await configure({
|
|
24
36
|
mounts: {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { InMemory } from '@zenfs/core';
|
|
3
|
+
import { withExceptionContext } from '@zenfs/core/internal/error';
|
|
4
|
+
import { cacheOf } from '@zenfs/core/vfs/vcache';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { suite, test } from 'node:test';
|
|
7
|
+
|
|
8
|
+
suite('VCache', () => {
|
|
9
|
+
test('does not keep the exception context of the first caller', () => {
|
|
10
|
+
const fs = InMemory.create({ label: 'vcache' });
|
|
11
|
+
assert.equal(cacheOf(withExceptionContext(fs, { path: '/first' })).fs, fs);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -67,7 +67,7 @@ suite('Concurrency', config('write', 'async'), () => {
|
|
|
67
67
|
const rejected = results.filter(r => r.status === 'rejected');
|
|
68
68
|
assert.deepEqual(
|
|
69
69
|
rejected.map(r => r.reason.code ?? r.reason.message),
|
|
70
|
-
[]
|
|
70
|
+
[],
|
|
71
71
|
);
|
|
72
72
|
|
|
73
73
|
assert.equal(await fs.promises.readFile('/issue-303.txt', 'utf8'), contents);
|
|
@@ -96,7 +96,7 @@ suite('Concurrency', config('write', 'async'), () => {
|
|
|
96
96
|
const rejected = results.filter(r => r.status == 'rejected');
|
|
97
97
|
assert.deepEqual(
|
|
98
98
|
rejected.map(r => r.reason.code ?? r.reason.message),
|
|
99
|
-
[]
|
|
99
|
+
[],
|
|
100
100
|
);
|
|
101
101
|
|
|
102
102
|
for (const path of paths) assert((await fs.promises.stat(path)).isDirectory());
|
|
@@ -107,7 +107,7 @@ suite('Concurrency', config('write', 'async'), () => {
|
|
|
107
107
|
|
|
108
108
|
assert.deepEqual(
|
|
109
109
|
results.filter(result => result !== undefined),
|
|
110
|
-
['/308-same']
|
|
110
|
+
['/308-same'],
|
|
111
111
|
);
|
|
112
112
|
});
|
|
113
113
|
});
|
package/tests/fs/links.test.ts
CHANGED
|
@@ -18,6 +18,16 @@ suite('Links', config('symlinks'), () => {
|
|
|
18
18
|
assert(stats.isSymbolicLink());
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
test('symlinks have mode 0o777 #326', async () => {
|
|
22
|
+
assert.equal((await fs.promises.lstat(symlink)).mode & 0o7777, 0o777);
|
|
23
|
+
|
|
24
|
+
fs.symlinkSync(target, 'symlink-sync.js');
|
|
25
|
+
const stats = fs.lstatSync('symlink-sync.js');
|
|
26
|
+
assert(stats.isSymbolicLink());
|
|
27
|
+
assert.equal(stats.mode & 0o7777, 0o777);
|
|
28
|
+
fs.unlinkSync('symlink-sync.js');
|
|
29
|
+
});
|
|
30
|
+
|
|
21
31
|
test('lstat file inside symlinked directory #241', async () => {
|
|
22
32
|
await fs.promises.mkdir('/a');
|
|
23
33
|
await fs.promises.writeFile('/a/hello.txt', 'hello world');
|
|
@@ -187,6 +197,72 @@ suite('Links', config('symlinks'), () => {
|
|
|
187
197
|
assert(fs.lstatSync('/wfS-dangling').isSymbolicLink());
|
|
188
198
|
});
|
|
189
199
|
|
|
200
|
+
test('operations follow symlinks in parent directories', config('async'), async () => {
|
|
201
|
+
await fs.promises.mkdir('/parent-real');
|
|
202
|
+
await fs.promises.symlink('/parent-real', '/parent-link');
|
|
203
|
+
await fs.promises.writeFile('/parent-real/unlink', 'x');
|
|
204
|
+
await fs.promises.writeFile('/parent-real/rename', 'x');
|
|
205
|
+
await fs.promises.mkdir('/parent-real/rmdir');
|
|
206
|
+
await fs.promises.symlink('/parent-real/renamed', '/parent-real/symlink');
|
|
207
|
+
await fs.promises.symlink('/parent-real/rmdir', '/parent-real/dir-link');
|
|
208
|
+
|
|
209
|
+
await fs.promises.unlink('/parent-link/unlink');
|
|
210
|
+
await fs.promises.rename('/parent-link/rename', '/parent-link/renamed');
|
|
211
|
+
assert.equal(await fs.promises.readlink('/parent-link/symlink'), '/parent-real/renamed');
|
|
212
|
+
await fs.promises.lutimes('/parent-link/symlink', 1, 1);
|
|
213
|
+
assert.equal((await fs.promises.lstat('/parent-real/symlink')).mtimeMs, 1000);
|
|
214
|
+
|
|
215
|
+
await assert.rejects(fs.promises.rmdir('/parent-link/dir-link'), { code: 'ENOTDIR' });
|
|
216
|
+
await fs.promises.unlink('/parent-link/dir-link');
|
|
217
|
+
await fs.promises.rmdir('/parent-link/rmdir');
|
|
218
|
+
|
|
219
|
+
assert.deepEqual((await fs.promises.readdir('/parent-real')).sort(), ['renamed', 'symlink']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('operations follow symlinks in parent directories (sync)', config('sync'), () => {
|
|
223
|
+
fs.mkdirSync('/parentS-real');
|
|
224
|
+
fs.symlinkSync('/parentS-real', '/parentS-link');
|
|
225
|
+
fs.writeFileSync('/parentS-real/unlink', 'x');
|
|
226
|
+
fs.writeFileSync('/parentS-real/rename', 'x');
|
|
227
|
+
fs.mkdirSync('/parentS-real/rmdir');
|
|
228
|
+
fs.symlinkSync('/parentS-real/renamed', '/parentS-real/symlink');
|
|
229
|
+
fs.symlinkSync('/parentS-real/rmdir', '/parentS-real/dir-link');
|
|
230
|
+
|
|
231
|
+
fs.unlinkSync('/parentS-link/unlink');
|
|
232
|
+
fs.renameSync('/parentS-link/rename', '/parentS-link/renamed');
|
|
233
|
+
assert.equal(fs.readlinkSync('/parentS-link/symlink'), '/parentS-real/renamed');
|
|
234
|
+
fs.lutimesSync('/parentS-link/symlink', 1, 1);
|
|
235
|
+
assert.equal(fs.lstatSync('/parentS-real/symlink').mtimeMs, 1000);
|
|
236
|
+
|
|
237
|
+
assert.throws(() => fs.rmdirSync('/parentS-link/dir-link'), { code: 'ENOTDIR' });
|
|
238
|
+
fs.unlinkSync('/parentS-link/dir-link');
|
|
239
|
+
fs.rmdirSync('/parentS-link/rmdir');
|
|
240
|
+
|
|
241
|
+
assert.deepEqual(fs.readdirSync('/parentS-real').sort(), ['renamed', 'symlink']);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('link follows symlinks in parent directories', config('links'), async () => {
|
|
245
|
+
await fs.promises.link('/parent-link/renamed', '/parent-link/hard');
|
|
246
|
+
assert.equal(await fs.promises.readFile('/parent-real/hard', 'utf8'), 'x');
|
|
247
|
+
fs.linkSync('/parentS-link/renamed', '/parentS-link/hard');
|
|
248
|
+
assert.equal(fs.readFileSync('/parentS-real/hard', 'utf8'), 'x');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('xattr follows symlinks unless noFollow is set', config('xattr'), async () => {
|
|
252
|
+
await fs.xattr.set('/parent-link/renamed', 'user.parent', 'async');
|
|
253
|
+
assert.equal(await fs.xattr.get('/parent-real/renamed', 'user.parent', { encoding: 'utf8' }), 'async');
|
|
254
|
+
fs.xattr.setSync('/parentS-link/renamed', 'user.parent', 'sync');
|
|
255
|
+
assert.equal(fs.xattr.getSync('/parentS-real/renamed', 'user.parent', { encoding: 'utf8' }), 'sync');
|
|
256
|
+
|
|
257
|
+
await fs.xattr.set('/parent-link/symlink', 'user.link', 'on link', { noFollow: true });
|
|
258
|
+
assert.equal(await fs.xattr.get('/parent-real/symlink', 'user.link', { encoding: 'utf8', noFollow: true }), 'on link');
|
|
259
|
+
await assert.rejects(fs.xattr.get('/parent-real/renamed', 'user.link'), { code: 'ENODATA' });
|
|
260
|
+
assert.deepEqual(await fs.xattr.list('/parent-link/symlink'), ['user.parent']);
|
|
261
|
+
|
|
262
|
+
await fs.xattr.remove('/parent-link/renamed', 'user.parent');
|
|
263
|
+
assert.deepEqual(fs.xattr.listSync('/parent-real/renamed'), []);
|
|
264
|
+
});
|
|
265
|
+
|
|
190
266
|
test('cp preserves a symlink by default #304', async () => {
|
|
191
267
|
await fs.promises.mkdir('/cp-src');
|
|
192
268
|
await fs.promises.writeFile('/cp-src/real.txt', 'contents');
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { bindContext, fs as rootFS } from '@zenfs/core';
|
|
2
3
|
import { R_OK, W_OK, X_OK } from '@zenfs/core/constants';
|
|
3
4
|
import { defaultContext } from '@zenfs/core/internal/contexts';
|
|
4
5
|
import { hasAccess } from '@zenfs/core/internal/inode';
|
|
5
6
|
import { join } from '@zenfs/core/path';
|
|
7
|
+
import * as vfsConfig from '@zenfs/core/vfs/config';
|
|
6
8
|
import type { Exception } from 'kerium';
|
|
7
9
|
import assert from 'node:assert/strict';
|
|
8
10
|
import { suite, test } from 'node:test';
|
|
@@ -84,6 +86,236 @@ suite('Permissions', config('permissions'), () => {
|
|
|
84
86
|
assert(hasAccess(defaultContext, stats, W_OK));
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
test('unprivileged users can read files they do not own #326', async () => {
|
|
90
|
+
await rootFS.promises.writeFile('/root-owned.txt', 'shared', { mode: 0o644 });
|
|
91
|
+
const user = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
92
|
+
|
|
93
|
+
assert.equal(user.fs.readFileSync('/root-owned.txt', 'utf8'), 'shared');
|
|
94
|
+
assert.equal(await user.fs.promises.readFile('/root-owned.txt', 'utf8'), 'shared');
|
|
95
|
+
|
|
96
|
+
assert.throws(() => user.fs.writeFileSync('/root-owned.txt', 'nope'), { code: 'EACCES' });
|
|
97
|
+
await assert.rejects(user.fs.promises.writeFile('/root-owned.txt', 'nope'), { code: 'EACCES' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('access checks follow Linux #326', () => {
|
|
101
|
+
const as = (uid: number, gid: number, euid = uid, egid = gid, groups: number[] = []) =>
|
|
102
|
+
bindContext({ credentials: { uid, gid, euid, egid, groups } });
|
|
103
|
+
const file = (mode: number, uid = 1000, gid = 1000) => ({ mode: 0o100000 | mode, uid, gid });
|
|
104
|
+
|
|
105
|
+
assert(!hasAccess(as(1000, 0), file(0o600, 0, 0), R_OK));
|
|
106
|
+
assert(hasAccess(as(0, 1000), file(0o000, 5, 5), R_OK | W_OK));
|
|
107
|
+
|
|
108
|
+
assert(hasAccess(as(1000, 1000, 2000, 1000), file(0o600, 2000, 5), R_OK | W_OK));
|
|
109
|
+
assert(!hasAccess(as(2000, 1000, 1000, 1000), file(0o600, 2000, 5), R_OK));
|
|
110
|
+
assert(!hasAccess(as(1000, 0, 1000, 1000), file(0o060, 2000, 0), R_OK));
|
|
111
|
+
|
|
112
|
+
assert(!hasAccess(as(1000, 1000), file(0o077), R_OK));
|
|
113
|
+
assert(!hasAccess(as(1000, 1000), file(0o407, 2000), R_OK));
|
|
114
|
+
assert(hasAccess(as(1000, 3000), file(0o004, 2000, 2000), R_OK));
|
|
115
|
+
assert(hasAccess(as(1000, 3000, 1000, 3000, [2000]), file(0o040, 2000, 2000), R_OK));
|
|
116
|
+
assert(hasAccess(as(1000, 2000), file(0o021, 0, 2000), W_OK));
|
|
117
|
+
assert(!hasAccess(as(1000, 2000), file(0o021, 0, 2000), X_OK));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('stat needs no permission on the file itself #326', async () => {
|
|
121
|
+
await rootFS.promises.writeFile('/secret-stat.txt', 'x', { mode: 0o000 });
|
|
122
|
+
const user = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
123
|
+
|
|
124
|
+
assert.equal(user.fs.statSync('/secret-stat.txt').size, 1);
|
|
125
|
+
assert.equal((await user.fs.promises.lstat('/secret-stat.txt')).size, 1);
|
|
126
|
+
assert.throws(() => user.fs.readFileSync('/secret-stat.txt'), { code: 'EACCES' });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('removing an entry needs write and search permission on its parent #326', async () => {
|
|
130
|
+
const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
131
|
+
|
|
132
|
+
await rootFS.promises.mkdir('/removal-root', { mode: 0o755 });
|
|
133
|
+
await rootFS.promises.writeFile('/removal-root/file', 'x', { mode: 0o666 });
|
|
134
|
+
await rootFS.promises.mkdir('/removal-root/dir', { mode: 0o777 });
|
|
135
|
+
await rootFS.promises.mkdir('/removal-own', { mode: 0o755 });
|
|
136
|
+
await rootFS.promises.lchown('/removal-own', 1000, 1000);
|
|
137
|
+
await rootFS.promises.writeFile('/removal-own/readonly', 'x', { mode: 0o444 });
|
|
138
|
+
await rootFS.promises.mkdir('/removal-own/readonly-dir', { mode: 0o555 });
|
|
139
|
+
await rootFS.promises.mkdir('/removal-unreadable', { mode: 0o300 });
|
|
140
|
+
await rootFS.promises.lchown('/removal-unreadable', 1000, 1000);
|
|
141
|
+
await rootFS.promises.writeFile('/removal-unreadable/file', 'x');
|
|
142
|
+
|
|
143
|
+
await assert.rejects(alice.fs.promises.unlink('/removal-root/file'), { code: 'EACCES' });
|
|
144
|
+
assert.throws(() => alice.fs.unlinkSync('/removal-root/file'), { code: 'EACCES' });
|
|
145
|
+
await assert.rejects(alice.fs.promises.rmdir('/removal-root/dir'), { code: 'EACCES' });
|
|
146
|
+
assert.throws(() => alice.fs.rmdirSync('/removal-root/dir'), { code: 'EACCES' });
|
|
147
|
+
assert.throws(() => alice.fs.renameSync('/removal-root/file', '/removal-own/file'), { code: 'EACCES' });
|
|
148
|
+
await assert.rejects(alice.fs.promises.rename('/removal-own/readonly', '/removal-root/file'), { code: 'EACCES' });
|
|
149
|
+
|
|
150
|
+
await alice.fs.promises.unlink('/removal-own/readonly');
|
|
151
|
+
alice.fs.rmdirSync('/removal-own/readonly-dir');
|
|
152
|
+
assert.deepEqual(rootFS.readdirSync('/removal-own'), []);
|
|
153
|
+
|
|
154
|
+
await alice.fs.promises.rename('/removal-unreadable/file', '/removal-unreadable/renamed');
|
|
155
|
+
alice.fs.renameSync('/removal-unreadable/renamed', '/removal-unreadable/file');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('sticky directories only let owners remove entries #326', async () => {
|
|
159
|
+
const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
160
|
+
const bob = bindContext({ credentials: { uid: 2000, gid: 2000 } });
|
|
161
|
+
|
|
162
|
+
await rootFS.promises.mkdir('/sticky', { mode: 0o1777 });
|
|
163
|
+
await alice.fs.promises.writeFile('/sticky/alice', 'a', { mode: 0o666 });
|
|
164
|
+
await alice.fs.promises.mkdir('/sticky/alice-dir', { mode: 0o777 });
|
|
165
|
+
await bob.fs.promises.writeFile('/sticky/bob', 'b');
|
|
166
|
+
|
|
167
|
+
await assert.rejects(bob.fs.promises.unlink('/sticky/alice'), { code: 'EPERM' });
|
|
168
|
+
assert.throws(() => bob.fs.unlinkSync('/sticky/alice'), { code: 'EPERM' });
|
|
169
|
+
await assert.rejects(bob.fs.promises.rmdir('/sticky/alice-dir'), { code: 'EPERM' });
|
|
170
|
+
assert.throws(() => bob.fs.rmdirSync('/sticky/alice-dir'), { code: 'EPERM' });
|
|
171
|
+
assert.throws(() => bob.fs.renameSync('/sticky/alice', '/sticky/stolen'), { code: 'EPERM' });
|
|
172
|
+
await assert.rejects(alice.fs.promises.rename('/sticky/alice', '/sticky/bob'), { code: 'EPERM' });
|
|
173
|
+
|
|
174
|
+
await alice.fs.promises.unlink('/sticky/alice');
|
|
175
|
+
alice.fs.rmdirSync('/sticky/alice-dir');
|
|
176
|
+
await rootFS.promises.unlink('/sticky/bob');
|
|
177
|
+
assert.deepEqual(rootFS.readdirSync('/sticky'), []);
|
|
178
|
+
|
|
179
|
+
await rootFS.promises.mkdir('/sticky-alice', { mode: 0o1777 });
|
|
180
|
+
await rootFS.promises.lchown('/sticky-alice', 1000, 1000);
|
|
181
|
+
await bob.fs.promises.writeFile('/sticky-alice/bob', 'b');
|
|
182
|
+
await alice.fs.promises.unlink('/sticky-alice/bob');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('only owners may change the mode, owner, or times of a file #326', async () => {
|
|
186
|
+
const alice = bindContext({ credentials: { uid: 1000, gid: 1000, groups: [3000] } });
|
|
187
|
+
const bob = bindContext({ credentials: { uid: 2000, gid: 2000 } });
|
|
188
|
+
|
|
189
|
+
await rootFS.promises.writeFile('/owned', 'x', { mode: 0o666 });
|
|
190
|
+
rootFS.chownSync('/owned', 1000, 1000);
|
|
191
|
+
|
|
192
|
+
assert.throws(() => bob.fs.chmodSync('/owned', 0o600), { code: 'EPERM' });
|
|
193
|
+
await assert.rejects(bob.fs.promises.chmod('/owned', 0o600), { code: 'EPERM' });
|
|
194
|
+
assert.throws(() => bob.fs.utimesSync('/owned', 1, 2), { code: 'EPERM' });
|
|
195
|
+
await assert.rejects(bob.fs.promises.utimes('/owned', 1, 2), { code: 'EPERM' });
|
|
196
|
+
assert.throws(() => bob.fs.chownSync('/owned', -1, 2000), { code: 'EPERM' });
|
|
197
|
+
assert.throws(() => bob.fs.chownSync('/owned', 1000, -1), { code: 'EPERM' });
|
|
198
|
+
bob.fs.chownSync('/owned', -1, -1);
|
|
199
|
+
|
|
200
|
+
alice.fs.chmodSync('/owned', 0o644);
|
|
201
|
+
await alice.fs.promises.utimes('/owned', 1, 2);
|
|
202
|
+
alice.fs.chownSync('/owned', -1, 3000);
|
|
203
|
+
await alice.fs.promises.chown('/owned', 1000, 1000);
|
|
204
|
+
assert.throws(() => alice.fs.chownSync('/owned', -1, 4000), { code: 'EPERM' });
|
|
205
|
+
assert.throws(() => alice.fs.chownSync('/owned', 2000, -1), { code: 'EPERM' });
|
|
206
|
+
|
|
207
|
+
rootFS.chownSync('/owned', 2000, 2000);
|
|
208
|
+
const stats = rootFS.statSync('/owned');
|
|
209
|
+
assert.equal(stats.mode & 0o777, 0o644);
|
|
210
|
+
assert.equal(stats.mtimeMs, 2000);
|
|
211
|
+
assert.equal(stats.uid, 2000);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('moving a directory to another parent needs write permission on it', async () => {
|
|
215
|
+
const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
216
|
+
|
|
217
|
+
await rootFS.promises.mkdir('/move-a', { mode: 0o777 });
|
|
218
|
+
await rootFS.promises.mkdir('/move-b', { mode: 0o777 });
|
|
219
|
+
await rootFS.promises.mkdir('/move-a/dir', { mode: 0o755 });
|
|
220
|
+
await rootFS.promises.writeFile('/move-a/file', 'x', { mode: 0o644 });
|
|
221
|
+
|
|
222
|
+
assert.throws(() => alice.fs.renameSync('/move-a/dir', '/move-b/dir'), { code: 'EACCES' });
|
|
223
|
+
await assert.rejects(alice.fs.promises.rename('/move-a/dir', '/move-b/dir'), { code: 'EACCES' });
|
|
224
|
+
await alice.fs.promises.rename('/move-a/dir', '/move-a/renamed');
|
|
225
|
+
await alice.fs.promises.rename('/move-a/file', '/move-b/file');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('chmod and chown clear setuid and setgid like Linux', async () => {
|
|
229
|
+
const alice = bindContext({ credentials: { uid: 1000, gid: 1000, groups: [3000] } });
|
|
230
|
+
const modeOf = (path: string) => rootFS.statSync(path).mode & 0o7777;
|
|
231
|
+
|
|
232
|
+
await rootFS.promises.mkdir('/sid', { mode: 0o777 });
|
|
233
|
+
await alice.fs.promises.writeFile('/sid/file', 'x');
|
|
234
|
+
await alice.fs.promises.mkdir('/sid/dir');
|
|
235
|
+
|
|
236
|
+
alice.fs.chmodSync('/sid/file', 0o6755);
|
|
237
|
+
assert.equal(modeOf('/sid/file'), 0o6755);
|
|
238
|
+
alice.fs.chownSync('/sid/file', -1, -1);
|
|
239
|
+
assert.equal(modeOf('/sid/file'), 0o755);
|
|
240
|
+
|
|
241
|
+
alice.fs.chmodSync('/sid/file', 0o2745);
|
|
242
|
+
alice.fs.chownSync('/sid/file', -1, 3000);
|
|
243
|
+
assert.equal(modeOf('/sid/file'), 0o2745);
|
|
244
|
+
|
|
245
|
+
rootFS.chmodSync('/sid/file', 0o4755);
|
|
246
|
+
rootFS.chownSync('/sid/file', 1000, 0);
|
|
247
|
+
assert.equal(modeOf('/sid/file'), 0o755);
|
|
248
|
+
|
|
249
|
+
alice.fs.chmodSync('/sid/file', 0o2755);
|
|
250
|
+
assert.equal(modeOf('/sid/file'), 0o755);
|
|
251
|
+
|
|
252
|
+
await alice.fs.promises.lchmod('/sid/dir', 0o6755);
|
|
253
|
+
await alice.fs.promises.lchown('/sid/dir', -1, -1);
|
|
254
|
+
assert.equal(modeOf('/sid/dir'), 0o6755);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('access checks see unsynced changes to an open file', async () => {
|
|
258
|
+
const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
259
|
+
|
|
260
|
+
await rootFS.promises.writeFile('/unsynced', 'x', { mode: 0o644 });
|
|
261
|
+
const fd = rootFS.openSync('/unsynced', 'r');
|
|
262
|
+
rootFS.fchmodSync(fd, 0o600);
|
|
263
|
+
|
|
264
|
+
assert.throws(() => alice.fs.readFileSync('/unsynced'), { code: 'EACCES' });
|
|
265
|
+
await assert.rejects(alice.fs.promises.readFile('/unsynced'), { code: 'EACCES' });
|
|
266
|
+
rootFS.closeSync(fd);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('reaching a path needs search permission on every directory leading to it #326', async () => {
|
|
270
|
+
const alice = bindContext({ credentials: { uid: 1000, gid: 1000 } });
|
|
271
|
+
const bob = bindContext({ credentials: { uid: 2000, gid: 2000 } });
|
|
272
|
+
|
|
273
|
+
await rootFS.promises.mkdir('/private', { mode: 0o700 });
|
|
274
|
+
await rootFS.promises.lchown('/private', 1000, 1000);
|
|
275
|
+
await rootFS.promises.mkdir('/private/inner', { mode: 0o755 });
|
|
276
|
+
await rootFS.promises.writeFile('/private/inner/readable.txt', 'secret', { mode: 0o644 });
|
|
277
|
+
await rootFS.promises.symlink('/private', '/private-link');
|
|
278
|
+
await rootFS.promises.mkdir('/search-only', { mode: 0o711 });
|
|
279
|
+
await rootFS.promises.writeFile('/search-only/known.txt', 'known', { mode: 0o644 });
|
|
280
|
+
await rootFS.promises.mkdir('/no-search', { mode: 0o722 });
|
|
281
|
+
|
|
282
|
+
assert.throws(() => bob.fs.readFileSync('/private-link/inner/readable.txt'), { code: 'EACCES' });
|
|
283
|
+
await assert.rejects(bob.fs.promises.readFile('/private-link/inner/readable.txt'), { code: 'EACCES' });
|
|
284
|
+
assert.throws(() => bob.fs.lstatSync('/private/inner'), { code: 'EACCES' });
|
|
285
|
+
assert.throws(() => bob.fs.mkdirSync('/no-search/dir'), { code: 'EACCES' });
|
|
286
|
+
assert.throws(() => bob.fs.writeFileSync('/no-search/file', 'x'), { code: 'EACCES' });
|
|
287
|
+
|
|
288
|
+
const { checkAccess, resolveFullWalk } = vfsConfig;
|
|
289
|
+
vfsConfig._setVFSConfig({ checkAccess, resolveFullWalk: true });
|
|
290
|
+
try {
|
|
291
|
+
assert.equal(alice.fs.readFileSync('/private/inner/readable.txt', 'utf8'), 'secret');
|
|
292
|
+
assert.equal(rootFS.readFileSync('/private/inner/readable.txt', 'utf8'), 'secret');
|
|
293
|
+
|
|
294
|
+
assert.throws(() => bob.fs.readFileSync('/private/inner/readable.txt'), { code: 'EACCES' });
|
|
295
|
+
await assert.rejects(bob.fs.promises.readFile('/private/inner/readable.txt'), { code: 'EACCES' });
|
|
296
|
+
assert.throws(() => bob.fs.statSync('/private/inner/readable.txt'), { code: 'EACCES' });
|
|
297
|
+
await assert.rejects(bob.fs.promises.stat('/private/inner/readable.txt'), { code: 'EACCES' });
|
|
298
|
+
assert.throws(() => bob.fs.readdirSync('/private/inner'), { code: 'EACCES' });
|
|
299
|
+
assert.throws(() => bob.fs.writeFileSync('/private/inner/new.txt', 'x'), { code: 'EACCES' });
|
|
300
|
+
assert.throws(() => bob.fs.unlinkSync('/private/inner/readable.txt'), { code: 'EACCES' });
|
|
301
|
+
assert.throws(() => bob.fs.mkdirSync('/private/inner/dir'), { code: 'EACCES' });
|
|
302
|
+
assert.throws(() => bob.fs.renameSync('/private/inner/readable.txt', '/stolen.txt'), { code: 'EACCES' });
|
|
303
|
+
assert(rootFS.existsSync('/private/inner/readable.txt'));
|
|
304
|
+
|
|
305
|
+
assert.equal(bob.fs.statSync('/private').mode & 0o777, 0o700);
|
|
306
|
+
assert.throws(() => bob.fs.readdirSync('/private'), { code: 'EACCES' });
|
|
307
|
+
|
|
308
|
+
assert.equal(bob.fs.readFileSync('/search-only/known.txt', 'utf8'), 'known');
|
|
309
|
+
assert.throws(() => bob.fs.readdirSync('/search-only'), { code: 'EACCES' });
|
|
310
|
+
|
|
311
|
+
assert.equal(bob.fs.existsSync('/private/inner/readable.txt'), false);
|
|
312
|
+
assert.equal(await bob.fs.promises.exists('/private/inner/readable.txt'), false);
|
|
313
|
+
assert.equal(bob.fs.existsSync('/search-only/known.txt'), true);
|
|
314
|
+
} finally {
|
|
315
|
+
vfsConfig._setVFSConfig({ checkAccess, resolveFullWalk });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
87
319
|
const copy = { ...defaultContext.credentials };
|
|
88
320
|
Object.assign(defaultContext.credentials, { uid: 1000, gid: 1000, euid: 1000, egid: 1000 });
|
|
89
321
|
test('Access controls: /', () => test_item('/'));
|
package/tests/fs/stat.test.ts
CHANGED
|
@@ -63,7 +63,7 @@ suite('Stats', () => {
|
|
|
63
63
|
assert.equal(hasAccess(defaultContext, stat, fs.constants.X_OK), false);
|
|
64
64
|
// changing group
|
|
65
65
|
|
|
66
|
-
Object.assign(defaultContext.credentials, { ...nonRootCredentials, gid: 44 });
|
|
66
|
+
Object.assign(defaultContext.credentials, { ...nonRootCredentials, gid: 44, egid: 44 });
|
|
67
67
|
|
|
68
68
|
assert.equal(hasAccess(defaultContext, stat, fs.constants.R_OK), false);
|
|
69
69
|
assert.equal(hasAccess(defaultContext, stat, fs.constants.W_OK), false);
|
package/tests/fs/streams.test.ts
CHANGED
package/tests/fs/xattr.test.ts
CHANGED
|
@@ -22,6 +22,16 @@ suite('Extended Attributes', config('xattr'), () => {
|
|
|
22
22
|
assert.equal(value, testValue);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test('set is kept when an open handle with unsynced changes is synced', async () => {
|
|
26
|
+
await using handle = await fs.promises.open(testFile, 'r');
|
|
27
|
+
await handle.chmod(0o640);
|
|
28
|
+
await fs.xattr.set(testFile, 'user.unsynced', 'value');
|
|
29
|
+
await handle.sync();
|
|
30
|
+
|
|
31
|
+
assert.equal(await fs.xattr.get(testFile, 'user.unsynced', { encoding: 'utf8' }), 'value');
|
|
32
|
+
assert.equal((await fs.promises.stat(testFile)).mode & 0o777, 0o640);
|
|
33
|
+
});
|
|
34
|
+
|
|
25
35
|
test('get attributes with buffer encoding', async () => {
|
|
26
36
|
await fs.xattr.set(testFile, 'user.buffer', 'buffer value');
|
|
27
37
|
const buffer = await fs.xattr.get(testFile, 'user.buffer', { encoding: 'buffer' });
|