@ai-ecoverse/slicc-kernel 1.64.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
@@ -561,6 +561,7 @@ With `users: 'enforce'` the kernel checks every file call that reaches it agains
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
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.
@@ -1484,6 +1484,11 @@ var Created = class {
1484
1484
  function deny(g, op, path, errno = "EACCES") {
1485
1485
  g.dac.denied({ pid: g.pid, uid: g.cred.euid, op, path, errno });
1486
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
+ }
1487
1492
  async function need(g, op, path, want, options = {}, shown = path) {
1488
1493
  const walked = await access(g.fs, g.cred, path, want, options);
1489
1494
  if (walked.denied !== void 0) deny(g, op, shown);
@@ -3196,10 +3201,18 @@ var UserDb = class {
3196
3201
  };
3197
3202
  }
3198
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) {
3199
3210
  const { passwd, group } = await this.tables();
3200
3211
  const key = typeof user === "number" ? "uid" : "name";
3201
3212
  const row = parsed(passwd).find((r) => r[key] === user);
3202
- 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 };
3203
3216
  }
3204
3217
  async list() {
3205
3218
  const { passwd, group } = await this.tables();
@@ -9816,6 +9829,7 @@ var KernelTty = class {
9816
9829
  waiters = [];
9817
9830
  hungUp = false;
9818
9831
  name;
9832
+ owner;
9819
9833
  onRead;
9820
9834
  readers = 0;
9821
9835
  screen;
@@ -10159,10 +10173,11 @@ var PtyTable = class {
10159
10173
  this.signal = signal;
10160
10174
  this.onRead = onRead;
10161
10175
  }
10162
- open() {
10176
+ open(owner) {
10163
10177
  let index = 0;
10164
10178
  while (this.pairs.has(index)) index += 1;
10165
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 };
10166
10181
  this.pairs.set(index, pair);
10167
10182
  return { pair, master: pair.master() };
10168
10183
  }
@@ -10175,6 +10190,10 @@ var PtyTable = class {
10175
10190
  return [...this.pairs.keys()].sort((a, b) => a - b);
10176
10191
  }
10177
10192
  };
10193
+ function ptsNumber(path) {
10194
+ const m = /^\/dev\/pts\/(\d+)$/.exec(path);
10195
+ return m ? Number(m[1]) : void 0;
10196
+ }
10178
10197
  var PTY_OPS = [
10179
10198
  "pty-open",
10180
10199
  "pty-slave-open",
@@ -10202,11 +10221,12 @@ function ptySyscall(req, ctx) {
10202
10221
  switch (req.op) {
10203
10222
  case "pty-open": {
10204
10223
  if (!ctx.ptys) throw new KernelError("ENOENT");
10205
- return json4(ctx.fds.install(ctx.ptys.open().master, 3));
10224
+ return json4(ctx.fds.install(ctx.ptys.open(ctx.owner).master, 3));
10206
10225
  }
10207
10226
  case "pty-slave-open": {
10208
10227
  if (!ctx.ptys) throw new KernelError("ENOENT");
10209
10228
  const pair = ctx.ptys.get(req.n);
10229
+ ttyOpenable(ctx.guard, pair.slave);
10210
10230
  const fd = ctx.fds.install(pair.openSlave(), 3);
10211
10231
  if (!req.noctty) ctx.jobs?.acquireTerminal(ctx.pid, pair.slave);
10212
10232
  return json4(fd);
@@ -11022,6 +11042,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
11022
11042
  "fd-promote",
11023
11043
  "fd-open-tty",
11024
11044
  "fd-tty-names",
11045
+ "fd-tty-owner",
11025
11046
  "tty-get",
11026
11047
  "tty-set",
11027
11048
  "tty-winsz",
@@ -11223,7 +11244,9 @@ var WasmProcess = class {
11223
11244
  }
11224
11245
  if (isPtySyscall(req)) {
11225
11246
  const { ptys, jobs } = this.options;
11226
- 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 });
11227
11250
  }
11228
11251
  return await this.procSyscall(req, cancelled);
11229
11252
  } catch (e) {
@@ -11355,6 +11378,25 @@ var WasmProcess = class {
11355
11378
  const want = (access2 === 1 ? 0 : R_OK) | (access2 !== 0 || req.truncate ? W_OK : 0);
11356
11379
  await openable(g, "open", req.path, want, req.create === true);
11357
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
+ }
11358
11400
  newOwner() {
11359
11401
  if (!this.options.owners) return void 0;
11360
11402
  return { uid: this.cred.euid, gid: this.cred.egid, mode: 438 & ~this.umask };
@@ -11458,13 +11500,12 @@ var WasmProcess = class {
11458
11500
  if (!req.keep) await Promise.resolve(this.fds.close(req.from));
11459
11501
  }
11460
11502
  return { ok: true, kind: "void" };
11461
- case "fd-open-tty": {
11462
- const tty = req.name === void 0 ? this.controllingTerminal() : this.options.jobs?.terminalNamed(req.name);
11463
- if (!tty) throw new KernelError("ENXIO");
11464
- return { ok: true, kind: "json", json: this.fds.install(tty.file(), 3) };
11465
- }
11503
+ case "fd-open-tty":
11504
+ return { ok: true, kind: "json", json: this.openTty(req.name) };
11466
11505
  case "fd-tty-names":
11467
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 };
11468
11509
  case "fd-select": {
11469
11510
  const { read, write, timeoutMs } = req;
11470
11511
  if (this.owed && timeoutMs !== 0) {
@@ -15086,7 +15127,12 @@ var Launcher = class {
15086
15127
  }
15087
15128
  async account(user) {
15088
15129
  if (typeof user === "object") return user;
15089
- 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);
15090
15136
  if (!found) throw fsError("ENOENT", `no user ${String(user)}`);
15091
15137
  return found;
15092
15138
  }
@@ -15108,7 +15154,7 @@ var Launcher = class {
15108
15154
  return { cwd, file, planned };
15109
15155
  }
15110
15156
  async openTerminal(argv, options) {
15111
- const account = await this.account(options.user);
15157
+ const { account, tty: ttyGroup } = await this.terminalAccount(options.user);
15112
15158
  const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
15113
15159
  if (!planned) throw new Error(`${file}: command not found`);
15114
15160
  let leader = 0;
@@ -15118,6 +15164,7 @@ var Launcher = class {
15118
15164
  );
15119
15165
  tty.onRead = () => this.settling.reading(this.jobs.tcgetpgrp(tty, leader));
15120
15166
  tty.name = `/dev/tty${++this.terminals}`;
15167
+ if (this.owners) tty.owner = { uid: account.uid, gid: ttyGroup ?? account.gid, mode: TTY_MODE };
15121
15168
  tty.setSize(options.cols ?? 80, options.rows ?? 24);
15122
15169
  const fds = new FdTable();
15123
15170
  const stdio = tty.file();