@volter/browser-wali 0.1.15 → 0.1.17

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.
Files changed (40) hide show
  1. package/dist/filesystem-host.d.ts.map +1 -1
  2. package/dist/filesystem-host.js +4 -2
  3. package/dist/filesystem-host.js.map +1 -1
  4. package/dist/filesystem-remote.d.ts +13 -1
  5. package/dist/filesystem-remote.d.ts.map +1 -1
  6. package/dist/filesystem-remote.js +72 -6
  7. package/dist/filesystem-remote.js.map +1 -1
  8. package/dist/filesystem.d.ts +27 -0
  9. package/dist/filesystem.d.ts.map +1 -1
  10. package/dist/filesystem.js +247 -7
  11. package/dist/filesystem.js.map +1 -1
  12. package/dist/network-worker.js +18 -11
  13. package/dist/network-worker.js.map +1 -1
  14. package/dist/network.d.ts +1 -0
  15. package/dist/network.d.ts.map +1 -1
  16. package/dist/network.js +4 -2
  17. package/dist/network.js.map +1 -1
  18. package/dist/private-tree.d.ts +63 -0
  19. package/dist/private-tree.d.ts.map +1 -0
  20. package/dist/private-tree.js +134 -0
  21. package/dist/private-tree.js.map +1 -0
  22. package/dist/runtime.d.ts.map +1 -1
  23. package/dist/runtime.js +38 -9
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/signals.d.ts +31 -2
  26. package/dist/signals.d.ts.map +1 -1
  27. package/dist/signals.js +35 -2
  28. package/dist/signals.js.map +1 -1
  29. package/dist/snapshot-filesystem.d.ts +9 -0
  30. package/dist/snapshot-filesystem.d.ts.map +1 -1
  31. package/dist/snapshot-filesystem.js +20 -0
  32. package/dist/snapshot-filesystem.js.map +1 -1
  33. package/dist/symlink.d.ts.map +1 -1
  34. package/dist/symlink.js +13 -3
  35. package/dist/symlink.js.map +1 -1
  36. package/dist/thread-worker.js +17 -5
  37. package/dist/thread-worker.js.map +1 -1
  38. package/dist/types.d.ts +11 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { WaliDescriptionRegistry, WaliProcessDescriptorTable } from "./descripto
3
3
  import { createProcessFilesystemMutationGuard } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
4
4
  import { BufferedWaliStdin } from "./stdin.js";
5
5
  import { readWaliSymlink, resolveWaliSymlinkPath, writeWaliSymlink } from "./symlink.js";
6
+ import { PR_WALI, PR_WALI_TREE_ADOPT, PR_WALI_TREE_CONFIRM, PR_WALI_TREE_DECLINE, PR_WALI_TREE_RELEASE, PRIVATE_TREE_INODES, PRIVATE_TREE_WINDOW_BASE, PRIVATE_TREE_WINDOW_SIZE, decodePrivateTree, encodePrivateTree, inPrivateTreeWindow, } from "./private-tree.js";
6
7
  let reportedDroppedEnvironment = false;
7
8
  const AT_FDCWD = -100;
8
9
  const O_ACCMODE = 3;
@@ -58,6 +59,9 @@ export class WaliFileDescriptorTable {
58
59
  cwd;
59
60
  guestFilesystem;
60
61
  assertWritable;
62
+ /** The private root the program's libc holds, while it holds it (ADR-0047). */
63
+ privateTree;
64
+ privateTreeDeclined = false;
61
65
  constructor(options) {
62
66
  this.options = options;
63
67
  this.descriptors = options.descriptors ?? new WaliProcessDescriptorTable(new WaliDescriptionRegistry());
@@ -93,6 +97,8 @@ export class WaliFileDescriptorTable {
93
97
  renameSync: (from, to) => fs.renameSync(this.assertWritable(from), this.assertWritable(to)),
94
98
  ...(fs.chmodSync ? { chmodSync: (path, mode) => fs.chmodSync(this.assertWritable(path), mode) } : {}),
95
99
  ...(fs.utimesSync ? { utimesSync: (path, atime, mtime) => fs.utimesSync(this.assertWritable(path), atime, mtime) } : {}),
100
+ ...(fs.reserveInodes ? { reserveInodes: fs.reserveInodes.bind(fs) } : {}),
101
+ ...(fs.restoreNodeSync ? { restoreNodeSync: (path, node) => fs.restoreNodeSync(this.assertWritable(path), node) } : {}),
96
102
  } : fs;
97
103
  this.stdin = "read" in options.stdin ? options.stdin : new BufferedWaliStdin(options.stdin);
98
104
  if (!options.descriptors) {
@@ -166,6 +172,9 @@ export class WaliFileDescriptorTable {
166
172
  SYS_fsync: (fd) => this.hasFd(Number(fd)) ? 0n : -9n,
167
173
  SYS_fdatasync: (fd) => this.hasFd(Number(fd)) ? 0n : -9n,
168
174
  SYS_flock: (fd, operation) => this.flock(Number(fd), Number(operation)),
175
+ // ADR-0047's handshake; every other prctl answers 0, as before.
176
+ SYS_prctl: (option, operation, pointer, length) => Number(option) === PR_WALI
177
+ ? this.privateTreeCall(Number(operation), Number(pointer), Number(length)) : 0n,
169
178
  };
170
179
  }
171
180
  writeEnvironmentFile(env, pid) {
@@ -462,6 +471,11 @@ export class WaliFileDescriptorTable {
462
471
  allocateFd(minimum = 3) {
463
472
  const maximum = this.options.descriptorMaximum ?? 65_535;
464
473
  for (let fd = Math.max(minimum, 0); fd <= maximum; fd += 1) {
474
+ // A private tree's numbers are its libc's, held or given back (ADR-0047).
475
+ if (inPrivateTreeWindow(fd)) {
476
+ fd = PRIVATE_TREE_WINDOW_BASE + PRIVATE_TREE_WINDOW_SIZE - 1;
477
+ continue;
478
+ }
465
479
  // `stdioFd` covers 0-2, this table's own aliases and the shared registry.
466
480
  if (this.descriptors.get(fd) || this.files.has(fd) || this.specialDescriptors.has(fd)
467
481
  || this.stdioFd(fd) !== undefined)
@@ -695,7 +709,8 @@ export class WaliFileDescriptorTable {
695
709
  throw errno("EINVAL");
696
710
  const base = whence === SEEK_SET ? 0 : whence === SEEK_CUR ? file.position
697
711
  : whence === SEEK_END ? this.fs().statSync(file.path).size : undefined;
698
- if (base === undefined || base + offset < 0)
712
+ // An offset or a position past 2^53 - 1 is not one a double holds (musl 0005's lseek says the same).
713
+ if (base === undefined || !Number.isSafeInteger(offset) || base + offset < 0 || !Number.isSafeInteger(base + offset))
699
714
  throw errno("EINVAL");
700
715
  file.position = base + offset;
701
716
  return BigInt(file.position);
@@ -935,7 +950,36 @@ export class WaliFileDescriptorTable {
935
950
  }
936
951
  }
937
952
  rename(from, to) {
938
- return this.mutate(() => this.fs().renameSync(this.resolveFilesystemPath(this.resolve(from), false), this.resolveFilesystemPath(this.resolve(to), false)));
953
+ return this.mutate(() => {
954
+ const source = this.resolveFilesystemPath(this.resolve(from), false);
955
+ const destination = this.resolveFilesystemPath(this.resolve(to), false);
956
+ // Moving a directory the held tree is under moves the tree: the libc's to release first (ADR-0047).
957
+ if (this.privateTree && (underRoot(this.privateTree.root, source) || underRoot(this.privateTree.root, destination)))
958
+ throw errno("EREMOTE");
959
+ const fs = this.fs();
960
+ // Linux's rules, before the store's move: a file never replaces a
961
+ // directory nor a directory a file, a directory only an empty one, and
962
+ // never into itself. The store's move sets the source over whatever the
963
+ // destination held and leaves that directory's children named under a
964
+ // file, a tree no later snapshot can be laid out from.
965
+ if (fs.existsSync(source)) {
966
+ if (source === destination)
967
+ return;
968
+ const directory = fs.statSync(source).isDirectory();
969
+ if (directory && destination.startsWith(`${source}/`))
970
+ throw errno("EINVAL");
971
+ if (fs.existsSync(destination)) {
972
+ const into = fs.statSync(destination).isDirectory();
973
+ if (directory && !into)
974
+ throw errno("ENOTDIR");
975
+ if (!directory && into)
976
+ throw errno("EISDIR");
977
+ if (directory && fs.readdirSync(destination).length > 0)
978
+ throw errno("ENOTEMPTY");
979
+ }
980
+ }
981
+ fs.renameSync(source, destination);
982
+ });
939
983
  }
940
984
  utimensat(dirfd, pathPointer, timesPointer, flags) {
941
985
  try {
@@ -984,6 +1028,8 @@ export class WaliFileDescriptorTable {
984
1028
  if (!Number.isSafeInteger(size) || size < 0)
985
1029
  throw errno("EINVAL");
986
1030
  const resolved = this.resolveAt(dirfd, path);
1031
+ if (this.privateTree && underRoot(resolved, this.privateTree.root))
1032
+ throw errno("EREMOTE");
987
1033
  let target;
988
1034
  if (resolved === "/proc/self/exe")
989
1035
  target = this.options.executablePath ?? "/usr/bin/wali-program";
@@ -1018,7 +1064,13 @@ export class WaliFileDescriptorTable {
1018
1064
  }
1019
1065
  }
1020
1066
  resolveFilesystemPath(path, followFinal = true) {
1021
- return resolveWaliSymlinkPath(this.fs(), path, followFinal);
1067
+ const resolved = resolveWaliSymlinkPath(this.fs(), path, followFinal);
1068
+ // A held private tree is the libc's: a path that reaches it here (a symlink
1069
+ // elsewhere, a directory descriptor) is EREMOTE, and the libc releases the
1070
+ // tree and asks again (ADR-0047 §3).
1071
+ if (this.privateTree && underRoot(resolved, this.privateTree.root))
1072
+ throw errno("EREMOTE");
1073
+ return resolved;
1022
1074
  }
1023
1075
  readSymlink(path) {
1024
1076
  return readWaliSymlink(this.fs(), path);
@@ -1028,7 +1080,7 @@ export class WaliFileDescriptorTable {
1028
1080
  const stdio = this.stdioFd(fd);
1029
1081
  if (stdio !== undefined) {
1030
1082
  const next = target ?? this.allocateFd(minimum);
1031
- if (next < 0 || next > maximum)
1083
+ if (next < 0 || (next > maximum && !inPrivateTreeWindow(next)))
1032
1084
  return -9n;
1033
1085
  if (next === fd)
1034
1086
  return BigInt(next);
@@ -1040,7 +1092,7 @@ export class WaliFileDescriptorTable {
1040
1092
  if (!this.descriptors.get(fd))
1041
1093
  return -9n;
1042
1094
  const next = target ?? this.allocateFd(minimum);
1043
- if (next < 0 || next > maximum)
1095
+ if (next < 0 || (next > maximum && !inPrivateTreeWindow(next)))
1044
1096
  return -9n;
1045
1097
  if (next === fd)
1046
1098
  return BigInt(next);
@@ -1283,6 +1335,194 @@ export class WaliFileDescriptorTable {
1283
1335
  return negativeErrno(error);
1284
1336
  }
1285
1337
  }
1338
+ privateTreeCall(operation, pointer, length) {
1339
+ try {
1340
+ if (operation === PR_WALI_TREE_ADOPT)
1341
+ return this.adoptPrivateTree(pointer, length);
1342
+ if (operation === PR_WALI_TREE_RELEASE)
1343
+ return this.releasePrivateTree(pointer, length);
1344
+ if (operation === PR_WALI_TREE_CONFIRM)
1345
+ return this.confirmPrivateTree();
1346
+ if (operation === PR_WALI_TREE_DECLINE) {
1347
+ // The libc could not take the tree: it stays the kernel's, as it was,
1348
+ // with anything a CONFIRM that failed part-way had removed put back.
1349
+ const handed = this.privateTree?.handed;
1350
+ if (!handed)
1351
+ return -22n;
1352
+ const fs = this.fs();
1353
+ for (const node of handed)
1354
+ if (!fs.existsSync(node.path)) {
1355
+ if (fs.restoreNodeSync)
1356
+ fs.restoreNodeSync(node.path, node);
1357
+ else {
1358
+ if (node.directory)
1359
+ fs.mkdirSync(node.path, { recursive: true });
1360
+ else
1361
+ fs.writeFileSync(node.path, node.content);
1362
+ fs.chmodSync?.(node.path, node.mode & 0o7777);
1363
+ fs.utimesSync?.(node.path, node.atimeMs, node.mtimeMs);
1364
+ }
1365
+ }
1366
+ this.privateTree = undefined;
1367
+ return 0n;
1368
+ }
1369
+ return -22n;
1370
+ }
1371
+ catch (error) {
1372
+ return negativeErrno(error);
1373
+ }
1374
+ }
1375
+ /**
1376
+ * ADOPT (ADR-0047 §3): the first private root, offered only while nothing
1377
+ * else can see it. The reply is the window, the inode range, the root, the
1378
+ * working directory, and what the kernel's tree already held there (a run's
1379
+ * environment file), which leaves the kernel's tree. 0 declines, as an
1380
+ * older kernel's answer to every prctl does.
1381
+ */
1382
+ adoptPrivateTree(pointer, length) {
1383
+ const decline = (why) => {
1384
+ if (!this.privateTreeDeclined) {
1385
+ this.privateTreeDeclined = true;
1386
+ this.options.onDiagnostic?.(`[WALI] the private tree is not the program's (ADR-0047): ${why}; its file calls cross into the kernel.`);
1387
+ }
1388
+ return 0n;
1389
+ };
1390
+ if (this.privateTree)
1391
+ return decline("it is held already");
1392
+ const root = this.options.privateRoots?.[0];
1393
+ if (!root)
1394
+ return decline("the run names no private root");
1395
+ if (root === "/")
1396
+ return decline("the root is the whole filesystem");
1397
+ if (this.options.privateTreeAdoptable && !this.options.privateTreeAdoptable())
1398
+ return decline("a forked process shares the filesystem");
1399
+ const fs = this.fs();
1400
+ for (let fd = 0; fd < this.descriptors.entries.length; fd += 1) {
1401
+ const file = this.files.get(fd);
1402
+ if (file && underRoot(file.path, root))
1403
+ return decline(`a descriptor is open under ${root}`);
1404
+ }
1405
+ try {
1406
+ this.assertWritable(`${root}/.wali-private-tree`);
1407
+ }
1408
+ catch {
1409
+ return decline(`the run's filesystem grant does not allow writes under ${root}`);
1410
+ }
1411
+ if (fs.existsSync(root) && (!fs.statSync(root).isDirectory() || readWaliSymlink(fs, root) !== undefined))
1412
+ return decline(`${root} is not a directory`);
1413
+ // Made only once nothing declines: the offer is the one change.
1414
+ if (!fs.existsSync(root))
1415
+ fs.mkdirSync(root, { recursive: true });
1416
+ const nodes = [];
1417
+ const walk = (path) => {
1418
+ const stat = fs.statSync(path);
1419
+ const directory = stat.isDirectory();
1420
+ nodes.push({ path, directory, mode: stat.mode ?? (directory ? 0o755 : 0o644), atimeMs: stat.atimeMs ?? stat.mtimeMs ?? 0, mtimeMs: stat.mtimeMs ?? 0, ino: stat.ino ?? 1, content: directory ? new Uint8Array() : fs.readFileSync(path) });
1421
+ if (directory)
1422
+ for (const name of fs.readdirSync(path))
1423
+ walk(`${path === "/" ? "" : path}/${name}`);
1424
+ };
1425
+ walk(root);
1426
+ const tree = encodePrivateTree(nodes);
1427
+ const encoder = new TextEncoder();
1428
+ const rootBytes = encoder.encode(root);
1429
+ const cwdBytes = encoder.encode(this.cwd);
1430
+ const size = 4 + 4 + 8 + 4 + rootBytes.byteLength + 4 + cwdBytes.byteLength + tree.byteLength;
1431
+ if (size > length)
1432
+ return decline(`what ${root} already holds is larger than the libc's ${length}-byte reply`);
1433
+ const inodes = fs.reserveInodes?.(PRIVATE_TREE_INODES) ?? 2 ** 40;
1434
+ const reply = new Uint8Array(size);
1435
+ const view = new DataView(reply.buffer);
1436
+ let at = 0;
1437
+ view.setUint32(at, PRIVATE_TREE_WINDOW_BASE, true);
1438
+ at += 4;
1439
+ view.setUint32(at, PRIVATE_TREE_WINDOW_SIZE, true);
1440
+ at += 4;
1441
+ view.setBigUint64(at, BigInt(inodes), true);
1442
+ at += 8;
1443
+ view.setUint32(at, rootBytes.byteLength, true);
1444
+ at += 4;
1445
+ reply.set(rootBytes, at);
1446
+ at += rootBytes.byteLength;
1447
+ view.setUint32(at, cwdBytes.byteLength, true);
1448
+ at += 4;
1449
+ reply.set(cwdBytes, at);
1450
+ at += cwdBytes.byteLength;
1451
+ reply.set(tree, at);
1452
+ this.copyIn(pointer, reply);
1453
+ // Held from here; the kernel's copy stays until the libc says it took it
1454
+ // (CONFIRM), so a libc that could not (DECLINE) loses nothing.
1455
+ this.privateTree = { root, handed: nodes };
1456
+ return BigInt(PRIVATE_TREE_WINDOW_BASE);
1457
+ }
1458
+ /** CONFIRM: the libc holds what ADOPT handed it, and the kernel's copy empties. */
1459
+ confirmPrivateTree() {
1460
+ const held = this.privateTree;
1461
+ if (!held?.handed)
1462
+ return -22n;
1463
+ const fs = this.fs();
1464
+ for (const node of held.handed.slice(1).reverse()) {
1465
+ if (node.directory)
1466
+ fs.rmdirSync(node.path);
1467
+ else
1468
+ fs.unlinkSync(node.path);
1469
+ }
1470
+ held.handed = undefined;
1471
+ return 0n;
1472
+ }
1473
+ /**
1474
+ * RELEASE (ADR-0047 §4): the tree and every descriptor on it, installed in
1475
+ * this one call: the same paths, modes, times and inodes, the same
1476
+ * descriptor numbers, positions and flags, and a dup's descriptors sharing
1477
+ * one description again. From here every call on it is this kernel's.
1478
+ */
1479
+ releasePrivateTree(pointer, length) {
1480
+ const held = this.privateTree;
1481
+ if (!held || held.handed)
1482
+ return -22n;
1483
+ assertRange(this.options.memory(), pointer, length);
1484
+ const { nodes, descriptors } = decodePrivateTree(this.copyOut(pointer, length), held.root);
1485
+ const fs = this.fs();
1486
+ for (const node of nodes) {
1487
+ // Made in the tree, so made by this process, as the kernel's own open
1488
+ // records: the first lock on it must not pull the page's (absent) copy
1489
+ // over it (WaliCoherentFileLockClient).
1490
+ if (!node.directory)
1491
+ this.options.fileLocks?.created?.(node.path);
1492
+ if (fs.restoreNodeSync)
1493
+ fs.restoreNodeSync(node.path, node);
1494
+ else {
1495
+ if (node.directory)
1496
+ fs.mkdirSync(node.path, { recursive: true });
1497
+ else
1498
+ fs.writeFileSync(node.path, node.content);
1499
+ fs.chmodSync?.(node.path, node.mode & 0o7777);
1500
+ fs.utimesSync?.(node.path, node.atimeMs, node.mtimeMs);
1501
+ }
1502
+ }
1503
+ for (const descriptor of descriptors) {
1504
+ if (this.descriptors.get(descriptor.fd))
1505
+ this.descriptors.close(descriptor.fd);
1506
+ if (descriptor.group === descriptor.fd) {
1507
+ this.files.set(descriptor.fd, {
1508
+ path: descriptor.path, flags: descriptor.flags, position: descriptor.position,
1509
+ directory: descriptor.directory, description: this.nextDescription++,
1510
+ });
1511
+ }
1512
+ else {
1513
+ const shared = this.descriptors.get(descriptor.group);
1514
+ if (!shared)
1515
+ throw errno("EINVAL");
1516
+ this.descriptors.set(descriptor.fd, shared);
1517
+ }
1518
+ this.descriptors.entries[descriptor.fd].flags = descriptor.closeOnExec ? 1 : 0;
1519
+ }
1520
+ this.privateTree = undefined;
1521
+ return 0n;
1522
+ }
1523
+ }
1524
+ function underRoot(path, root) {
1525
+ return path === root || path.startsWith(root === "/" ? "/" : `${root}/`);
1286
1526
  }
1287
1527
  function normalizePath(cwd, path) {
1288
1528
  if (!path)
@@ -1437,10 +1677,10 @@ function errno(errnoName) {
1437
1677
  return error;
1438
1678
  }
1439
1679
  const ERRNO_NUMBERS = {
1440
- EPERM: 1n, ENOENT: 2n, EIO: 5n, EAGAIN: 11n, ENOMEM: 12n, EACCES: 13n,
1680
+ EPERM: 1n, ENOENT: 2n, EIO: 5n, EBADF: 9n, EAGAIN: 11n, ENOMEM: 12n, EACCES: 13n,
1441
1681
  EFAULT: 14n, EBUSY: 16n, EEXIST: 17n, EXDEV: 18n, ENOTDIR: 20n, EISDIR: 21n,
1442
1682
  EINVAL: 22n, EMFILE: 24n, ENOSPC: 28n, ESPIPE: 29n, EROFS: 30n, ERANGE: 34n,
1443
- ENOSYS: 38n, ENOTEMPTY: 39n, ELOOP: 40n, ENAMETOOLONG: 36n,
1683
+ ENOSYS: 38n, ENOTEMPTY: 39n, ELOOP: 40n, ENAMETOOLONG: 36n, EREMOTE: 66n,
1444
1684
  };
1445
1685
  function negativeErrno(error) {
1446
1686
  // The errno's own name first, and the message parsed only for an error that has none.