@ai-ecoverse/slicc-kernel 1.63.0 → 1.64.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -560,7 +560,8 @@ With `users: 'enforce'` the kernel checks every file call that reaches it agains
560
560
  - **Opening a file:** an open needs `r` to read and `w` to write or truncate, checked once when the file is opened, in every runtime: Emscripten, WASI (a second open of a path the program already has open is checked as well), JS programs' `ctx.open` and `ctx.fs.writeFile`, and the kernel's own opens. A file the open itself creates is not checked (only its directory is), so `open(O_CREAT | O_WRONLY, 0444)` works. An open descriptor keeps the access it was opened with, through a later `chmod`, and so do descriptors a child inherits. Reads and writes without a descriptor (`ctx.fs.readFile`, `ctx.sync.fs.readFile`) are checked each time.
561
561
  - **`access(2)`:** `faccessat` (bash's `[ -r ]`, `test -w`) answers from the same checks, with the real uid and gid, or the effective ones with `AT_EACCESS`, for Emscripten programs, and for WASI programs through `slicc_fs.path_access` (see [File modes and the umask](#file-modes-and-the-umask)).
562
562
  - **Exec:** running a program by path needs `x` on it, and `r` too when it's a script (`#!` or a `binfmt` suffix); a WASI or Emscripten binary needs only `x`. Root needs an `x` bit as well. Commands found by name, and `/usr/bin` and `/bin`, are always executable; a user's own commands in `~/.local/share/pnpm/bin` are reached through the user's home, so another user gets `EACCES` there, as for any file. A refused exec is `EACCES` (126 in bash).
563
- - **`/tmp` and `/dev`:** with `users: 'report'` or `'enforce'`, `/tmp` is `1777` (set again at every start), and Emscripten programs see `/dev` at `0755`, `null`, `zero`, `full`, `random`, `urandom` and `tty` at `0666`, and `/dev/shm` at `1777`. With `users: 'off'` they keep their modes as before.
563
+ - **The layout:** with `users: 'report'` or `'enforce'`, the kernel gives its own paths an owner and a mode when they have no owner record yet: on a fresh root, and once on a root from before owners. These are `/` `0755`, `/root` `0700`, `/home` `0755`, `/etc` `0755`, `/etc/passwd` and `/etc/group` `0644`, `/usr` and `/usr/local` `0755`, `/node_modules` (or the `modules` option) and the system `PNPM_HOME` `0755`, all root's. A path that has a record keeps it, so an admin's `chmod 711 /home` or `chown cone /node_modules` lasts across restarts; a mounted path is left to its mount, and a path below a symlinked directory is stamped where it really is. Only a root that records owners is stamped: the OPFS root with its sidecar, which the page and the Node entry (in memory) both use; a custom root file system without owner records is left as it is. The directory for files removed while open, `/.slicc-unlinked`, is made root's `0700`. `/tmp` is the exception: it is set to `1777` at every start. Emscripten programs see `/` at `0755`, `/dev` at `0755`, `null`, `zero`, `full`, `random`, `urandom` and `tty` at `0666`, and `/dev/shm` at `1777`. With `users: 'off'` nothing is stamped. Paths the embedder makes (bios's `/opt` and `/var/lib/slicc`) are the embedder's to set, and `/etc/sudoers` comes with `sudo`.
564
+ - **Terminals:** a terminal from `openTerminal` belongs to its session's user, with group `tty` when `/etc/group` has one (the user's own group otherwise), mode `0620`; a pseudo-terminal's slave `/dev/pts/N` belongs to the uid and gid that opened `/dev/ptmx`, mode `0620`. Opening one by name (`/dev/ttyN`, `/dev/pts/N`, from every runtime) needs both `r` and `w`, so only its owner and root open it: the kernel's terminal descriptor always reads and writes, so the group's `w` bit (`write`, `wall` on Linux) does not let a group member open it. `/dev/tty` is the opener's own controlling terminal and is not checked, and descriptors a process inherited (across `sudo` or a `setresuid` too) keep working. Emscripten programs' `stat` of `/dev/ttyN` and `/dev/pts/N` shows the owner and mode. With `users: 'off'` terminals have no owner, as before.
564
565
  - **A client's `fs`:** checked as the client's user; a page's client (root) passes.
565
566
 
566
567
  With `users: 'enforce'` not yet the default, no release turns it on before `sudo` exists.
@@ -427,6 +427,9 @@ var OpfsFs = class {
427
427
  trackOwners() {
428
428
  this.owners = true;
429
429
  }
430
+ async hasOwner(path) {
431
+ return ownOf((await this.meta.get([path]))[0]) !== void 0;
432
+ }
430
433
  async ancestry(path) {
431
434
  const entries = await this.meta.get(chain(path));
432
435
  const out = [];
@@ -1481,6 +1484,11 @@ var Created = class {
1481
1484
  function deny(g, op, path, errno = "EACCES") {
1482
1485
  g.dac.denied({ pid: g.pid, uid: g.cred.euid, op, path, errno });
1483
1486
  }
1487
+ var TTY_MODE = 400;
1488
+ function ttyOpenable(g, tty) {
1489
+ if (!g || !tty.owner || permits(g.cred, tty.owner, R_OK | W_OK, false)) return;
1490
+ deny(g, "open", tty.name ?? "/dev/tty");
1491
+ }
1484
1492
  async function need(g, op, path, want, options = {}, shown = path) {
1485
1493
  const walked = await access(g.fs, g.cred, path, want, options);
1486
1494
  if (walked.denied !== void 0) deny(g, op, shown);
@@ -3193,10 +3201,18 @@ var UserDb = class {
3193
3201
  };
3194
3202
  }
3195
3203
  async lookup(user) {
3204
+ return (await this.lookupTerminal(user))?.account;
3205
+ }
3206
+ async ttyGroup() {
3207
+ return parsed(parseGroup(await this.read(GROUP, DEFAULT_GROUP))).find((g) => g.name === "tty")?.gid;
3208
+ }
3209
+ async lookupTerminal(user) {
3196
3210
  const { passwd, group } = await this.tables();
3197
3211
  const key = typeof user === "number" ? "uid" : "name";
3198
3212
  const row = parsed(passwd).find((r) => r[key] === user);
3199
- return row && this.account(row, parsed(group));
3213
+ const groups = parsed(group);
3214
+ const tty = groups.find((g) => g.name === "tty")?.gid;
3215
+ return row && { account: this.account(row, groups), tty };
3200
3216
  }
3201
3217
  async list() {
3202
3218
  const { passwd, group } = await this.tables();
@@ -6427,6 +6443,7 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
6427
6443
  ...fs.realpath ? { realpath: fs.realpath.bind(fs) } : {},
6428
6444
  ...fs.chown ? { chown: fs.chown.bind(fs) } : {},
6429
6445
  ...fs.ancestry ? { ancestry: fs.ancestry.bind(fs) } : {},
6446
+ ...fs.hasOwner ? { hasOwner: fs.hasOwner.bind(fs) } : {},
6430
6447
  chmod: (path, mode) => fs.chmod(path, mode),
6431
6448
  utimes: (path, atime, mtime) => fs.utimes(path, atime, mtime),
6432
6449
  lutimes: (path, atime, mtime) => lutimesOf(fs, path, atime, mtime),
@@ -6526,8 +6543,10 @@ var UnlinkedKeeper = class {
6526
6543
  count = 0;
6527
6544
  links = /* @__PURE__ */ new Map();
6528
6545
  linked = /* @__PURE__ */ new Map();
6546
+ dirOwner;
6529
6547
  constructor(fs, holders, rootOf, options = {}) {
6530
6548
  const { locks, owner } = options;
6549
+ this.dirOwner = options.dirOwner;
6531
6550
  this.fs = fs;
6532
6551
  this.holders = holders;
6533
6552
  this.rootOf = rootOf;
@@ -6638,7 +6657,8 @@ var UnlinkedKeeper = class {
6638
6657
  const dir = dirOf(this.rootOf(path));
6639
6658
  const hidden = `${dir}/${this.instance}.${++this.count}`;
6640
6659
  try {
6641
- await this.fs.mkdir(dir, { recursive: true });
6660
+ const owner = this.dirOwner;
6661
+ await this.fs.mkdir(dir, { recursive: true, ...owner ? { owner } : {} });
6642
6662
  await this.fs.rename(path, hidden);
6643
6663
  await this.linkedName(hidden);
6644
6664
  return hidden;
@@ -8017,6 +8037,10 @@ var MountFs = class {
8017
8037
  if (this.at(real)) throw errnoError("ENOSYS", path);
8018
8038
  return real;
8019
8039
  }
8040
+ async hasOwner(path) {
8041
+ if (this.at(path)) return true;
8042
+ return await this.base.hasOwner?.(path) ?? true;
8043
+ }
8020
8044
  async ancestry(path) {
8021
8045
  const names = chain(normalizePath(path));
8022
8046
  const first = names.findIndex((name2) => this.at(name2));
@@ -9805,6 +9829,7 @@ var KernelTty = class {
9805
9829
  waiters = [];
9806
9830
  hungUp = false;
9807
9831
  name;
9832
+ owner;
9808
9833
  onRead;
9809
9834
  readers = 0;
9810
9835
  screen;
@@ -10148,10 +10173,11 @@ var PtyTable = class {
10148
10173
  this.signal = signal;
10149
10174
  this.onRead = onRead;
10150
10175
  }
10151
- open() {
10176
+ open(owner) {
10152
10177
  let index = 0;
10153
10178
  while (this.pairs.has(index)) index += 1;
10154
10179
  const pair = new PtyPair(index, this.signal, (p) => this.pairs.delete(p.index), this.onRead);
10180
+ if (owner) pair.slave.owner = { uid: owner.uid, gid: owner.gid, mode: TTY_MODE };
10155
10181
  this.pairs.set(index, pair);
10156
10182
  return { pair, master: pair.master() };
10157
10183
  }
@@ -10164,6 +10190,10 @@ var PtyTable = class {
10164
10190
  return [...this.pairs.keys()].sort((a, b) => a - b);
10165
10191
  }
10166
10192
  };
10193
+ function ptsNumber(path) {
10194
+ const m = /^\/dev\/pts\/(\d+)$/.exec(path);
10195
+ return m ? Number(m[1]) : void 0;
10196
+ }
10167
10197
  var PTY_OPS = [
10168
10198
  "pty-open",
10169
10199
  "pty-slave-open",
@@ -10191,11 +10221,12 @@ function ptySyscall(req, ctx) {
10191
10221
  switch (req.op) {
10192
10222
  case "pty-open": {
10193
10223
  if (!ctx.ptys) throw new KernelError("ENOENT");
10194
- return json4(ctx.fds.install(ctx.ptys.open().master, 3));
10224
+ return json4(ctx.fds.install(ctx.ptys.open(ctx.owner).master, 3));
10195
10225
  }
10196
10226
  case "pty-slave-open": {
10197
10227
  if (!ctx.ptys) throw new KernelError("ENOENT");
10198
10228
  const pair = ctx.ptys.get(req.n);
10229
+ ttyOpenable(ctx.guard, pair.slave);
10199
10230
  const fd = ctx.fds.install(pair.openSlave(), 3);
10200
10231
  if (!req.noctty) ctx.jobs?.acquireTerminal(ctx.pid, pair.slave);
10201
10232
  return json4(fd);
@@ -11011,6 +11042,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
11011
11042
  "fd-promote",
11012
11043
  "fd-open-tty",
11013
11044
  "fd-tty-names",
11045
+ "fd-tty-owner",
11014
11046
  "tty-get",
11015
11047
  "tty-set",
11016
11048
  "tty-winsz",
@@ -11212,7 +11244,9 @@ var WasmProcess = class {
11212
11244
  }
11213
11245
  if (isPtySyscall(req)) {
11214
11246
  const { ptys, jobs } = this.options;
11215
- return ptySyscall(req, { pid: this.pid, fds: this.fds, ptys, jobs });
11247
+ const owner = this.options.owners ? this.ttyOwner() : void 0;
11248
+ const guard2 = this.guarded();
11249
+ return ptySyscall(req, { pid: this.pid, fds: this.fds, ptys, jobs, guard: guard2, owner });
11216
11250
  }
11217
11251
  return await this.procSyscall(req, cancelled);
11218
11252
  } catch (e) {
@@ -11344,6 +11378,25 @@ var WasmProcess = class {
11344
11378
  const want = (access2 === 1 ? 0 : R_OK) | (access2 !== 0 || req.truncate ? W_OK : 0);
11345
11379
  await openable(g, "open", req.path, want, req.create === true);
11346
11380
  }
11381
+ openTty(name2) {
11382
+ const tty = name2 === void 0 ? this.controllingTerminal() : this.options.jobs?.terminalNamed(name2);
11383
+ if (!tty) throw new KernelError("ENXIO");
11384
+ if (name2 !== void 0) ttyOpenable(this.guarded(), tty);
11385
+ return this.fds.install(tty.file(), 3);
11386
+ }
11387
+ guarded() {
11388
+ const { guard: guard2 } = this.options;
11389
+ return guard2 && { fs: guard2.fs, dac: guard2.dac, cred: this.cred, pid: this.pid };
11390
+ }
11391
+ ttyOwner() {
11392
+ return { uid: this.cred.euid, gid: this.cred.egid, mode: TTY_MODE };
11393
+ }
11394
+ terminalByName(name2) {
11395
+ const n = ptsNumber(name2);
11396
+ const tty = n === void 0 ? this.options.jobs?.terminalNamed(name2) : this.options.ptys?.get(n).slave;
11397
+ if (!tty) throw new KernelError("ENOENT");
11398
+ return tty;
11399
+ }
11347
11400
  newOwner() {
11348
11401
  if (!this.options.owners) return void 0;
11349
11402
  return { uid: this.cred.euid, gid: this.cred.egid, mode: 438 & ~this.umask };
@@ -11447,13 +11500,12 @@ var WasmProcess = class {
11447
11500
  if (!req.keep) await Promise.resolve(this.fds.close(req.from));
11448
11501
  }
11449
11502
  return { ok: true, kind: "void" };
11450
- case "fd-open-tty": {
11451
- const tty = req.name === void 0 ? this.controllingTerminal() : this.options.jobs?.terminalNamed(req.name);
11452
- if (!tty) throw new KernelError("ENXIO");
11453
- return { ok: true, kind: "json", json: this.fds.install(tty.file(), 3) };
11454
- }
11503
+ case "fd-open-tty":
11504
+ return { ok: true, kind: "json", json: this.openTty(req.name) };
11455
11505
  case "fd-tty-names":
11456
11506
  return { ok: true, kind: "json", json: this.options.jobs?.terminalNames() ?? [] };
11507
+ case "fd-tty-owner":
11508
+ return { ok: true, kind: "json", json: this.terminalByName(req.name).owner ?? null };
11457
11509
  case "fd-select": {
11458
11510
  const { read, write, timeoutMs } = req;
11459
11511
  if (this.owed && timeoutMs !== 0) {
@@ -14126,6 +14178,16 @@ var NOT_FOUND = 127;
14126
14178
  var DECIDE_MS = 100;
14127
14179
  var INIT_PID = 1;
14128
14180
  var SHARED_DIRS = ["/tmp", "/home"];
14181
+ var LAYOUT = [
14182
+ ["/", 493],
14183
+ ["/root", 448],
14184
+ ["/home", 493],
14185
+ ["/etc", 493],
14186
+ [PASSWD, 420],
14187
+ [GROUP, 420],
14188
+ ["/usr", 493],
14189
+ ["/usr/local", 493]
14190
+ ];
14129
14191
  var encoder3 = new TextEncoder();
14130
14192
  var USERS_MODES = ["off", "report", "enforce"];
14131
14193
  function usersMode(mode = "off") {
@@ -14319,7 +14381,8 @@ var Launcher = class {
14319
14381
  {
14320
14382
  locks: webLocks(),
14321
14383
  busy: (path) => [...this.openFiles].some((nodes) => nodes.holds(path)),
14322
- owner: options.unlinkOwner
14384
+ owner: options.unlinkOwner,
14385
+ ...this.owners ? { dirOwner: { uid: 0, gid: 0, mode: 448 } } : {}
14323
14386
  }
14324
14387
  );
14325
14388
  this.fs = this.unlinked.wrap(keepingOpen(fs, this.nodes));
@@ -14944,6 +15007,24 @@ var Launcher = class {
14944
15007
  await this.provisionPnpmHome();
14945
15008
  return entry;
14946
15009
  }
15010
+ async stampLayout() {
15011
+ const layout = [
15012
+ ...LAYOUT,
15013
+ [this.modulesDir, 493],
15014
+ [this.pnpmHome, 493],
15015
+ [`/${UNLINKED_DIR}`, 448]
15016
+ ];
15017
+ for (const [path, mode] of layout) {
15018
+ const real = await this.realLayoutPath(path);
15019
+ const fresh = real !== void 0 && !await this.base.hasOwner?.(real);
15020
+ if (fresh) await this.base.chown?.(real, { uid: 0, gid: 0, mode, follow: false });
15021
+ }
15022
+ }
15023
+ async realLayoutPath(path) {
15024
+ if (!await this.base.exists(path)) return void 0;
15025
+ if (path === "/") return path;
15026
+ return joinName(await realDir(this.base, parentOf3(path)), baseName(path));
15027
+ }
14947
15028
  async provisionPnpmHome() {
14948
15029
  await this.base.mkdir(this.pnpmHome, { recursive: true }).catch(() => void 0);
14949
15030
  }
@@ -15017,6 +15098,7 @@ var Launcher = class {
15017
15098
  if (this.owners) await this.base.chmod("/tmp", 1023);
15018
15099
  await this.provisionPnpmHome();
15019
15100
  await this.users.ensure();
15101
+ if (this.owners) await this.stampLayout();
15020
15102
  await writeCaFile(this.base, this.ca).catch(() => void 0);
15021
15103
  this.fstab = this.mountFstab();
15022
15104
  }
@@ -15045,7 +15127,12 @@ var Launcher = class {
15045
15127
  }
15046
15128
  async account(user) {
15047
15129
  if (typeof user === "object") return user;
15048
- const found = await this.users.lookup(user ?? 0);
15130
+ return (await this.terminalAccount(user)).account;
15131
+ }
15132
+ async terminalAccount(user) {
15133
+ if (typeof user === "object")
15134
+ return { account: user, tty: this.owners ? await this.users.ttyGroup() : void 0 };
15135
+ const found = await this.users.lookupTerminal(user ?? 0);
15049
15136
  if (!found) throw fsError("ENOENT", `no user ${String(user)}`);
15050
15137
  return found;
15051
15138
  }
@@ -15067,7 +15154,7 @@ var Launcher = class {
15067
15154
  return { cwd, file, planned };
15068
15155
  }
15069
15156
  async openTerminal(argv, options) {
15070
- const account = await this.account(options.user);
15157
+ const { account, tty: ttyGroup } = await this.terminalAccount(options.user);
15071
15158
  const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
15072
15159
  if (!planned) throw new Error(`${file}: command not found`);
15073
15160
  let leader = 0;
@@ -15077,6 +15164,7 @@ var Launcher = class {
15077
15164
  );
15078
15165
  tty.onRead = () => this.settling.reading(this.jobs.tcgetpgrp(tty, leader));
15079
15166
  tty.name = `/dev/tty${++this.terminals}`;
15167
+ if (this.owners) tty.owner = { uid: account.uid, gid: ttyGroup ?? account.gid, mode: TTY_MODE };
15080
15168
  tty.setSize(options.cols ?? 80, options.rows ?? 24);
15081
15169
  const fds = new FdTable();
15082
15170
  const stdio = tty.file();