@ai-ecoverse/slicc-kernel 1.73.0 → 1.73.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
@@ -95,7 +95,7 @@ The user database, `/etc/passwd` and `/etc/group` (see [Users](#users)):
95
95
  | | |
96
96
  | --- | --- |
97
97
  | `mode` | the `users` option the kernel was created with |
98
- | `add({ name, home?, groups?, umask?, homeGroup?, homeMode? })` | adds a user and resolves with `{ name, uid, gid, home, shell, groups, umask? }`. It's idempotent: an existing name resolves with its entry, keeps its ids and home, joins the groups in `groups` it isn't in yet (it never leaves one), remembers `umask`, creates a missing pnpm home or `config.yaml` (an existing pnpm home keeps its mode with `users: 'off'`), and, with owners on, gives the home to the user again. The new user gets the lowest free id from 1000 up, a private group of the same id and name, membership of `users` and of each group in `groups` (such as `sudo`), `home` (default `/home/<name>`, created), its pnpm home `~/.local/share/pnpm` (`0700`, `0750` for a `scoop-` user) and `~/.config/pnpm/config.yaml` with `minimumReleaseAge: 1440` (pnpm skips releases less than a day old), each only if missing, which an `add` of an existing user also does, and `/bin/bash`. `umask` is the umask its processes start with; the kernel keeps it in memory, so pass it again after a reload. With owners on, every `add`, of a new or an existing user, also gives a home under `/home/<dir>` to the user and sets its pnpm home to `0700` (`0750` for a `scoop-` user) again (see [File owners](#file-owners)). `homeGroup` names the group the home belongs to instead of the user's own, without making the user a member (`groups` does that), and its mode is then `2750`. `homeMode` sets the home's mode instead (setgid included, so `0o2750` makes new files in it take the home's group); like the owner, it is set again by every `add` with owners on, on the home itself only. A name must match `[a-z_][a-z0-9_.-]*`; a group that doesn't exist, in `groups` or `homeGroup`, rejects with `ENOENT` |
98
+ | `add({ name, home?, groups?, umask?, homeGroup?, homeMode? })` | adds a user and resolves with `{ name, uid, gid, home, shell, groups, umask? }`. It's idempotent: an existing name resolves with its entry, keeps its ids and home, joins the groups in `groups` it isn't in yet (it never leaves one), remembers `umask`, creates a missing pnpm home or `config.yaml` (an existing pnpm home keeps its mode with `users: 'off'`), and, with owners on, gives the home to the user again. The new user gets the lowest free id from 1000 up, a private group of the same id and name, membership of `users` and of each group in `groups` (such as `sudo`), `home` (default `/home/<name>`, created), its pnpm home `~/.local/share/pnpm` (`0700`, `0750` for a `scoop-` user) and `~/.config/pnpm/config.yaml` with `minimumReleaseAge: 1440` (pnpm skips releases less than a day old), each only if missing (with owners on, the directories it makes on the way, `~/.config` and `~/.local` included, get `0777` less the user's `umask`, the file `0666` less it, and in a home with `homeGroup` they belong to that group and have the setgid bit, as new directories in a setgid directory do), which an `add` of an existing user also does, and `/bin/bash`. `umask` is the umask its processes start with; the kernel keeps it in memory, so pass it again after a reload. With owners on, every `add`, of a new or an existing user, also gives a home under `/home/<dir>` to the user and sets its pnpm home to `0700` (`0750` for a `scoop-` user) again (see [File owners](#file-owners)). `homeGroup` names the group the home belongs to instead of the user's own, without making the user a member (`groups` does that), and its mode is then `2750`. `homeMode` sets the home's mode instead (setgid included, so `0o2750` makes new files in it take the home's group); like the owner, it is set again by every `add` with owners on, on the home itself only. A name must match `[a-z_][a-z0-9_.-]*`; a group that doesn't exist, in `groups` or `homeGroup`, rejects with `ENOENT` |
99
99
  | `remove(name, { keepHome }?)` | removes the user, its private group and its group memberships, and deletes its home unless `keepHome` is set. A home outside `/home/<dir>` is never deleted. Resolves `false` for an unknown name; system accounts (ids below 1000 or above 60000) reject with `EPERM` |
100
100
  | `list()` | every user, as `add` returns them |
101
101
 
@@ -518,7 +518,7 @@ An exec'd program takes over the pid of the process it replaces, as on Linux: `g
518
518
 
519
519
  ### File modes and the umask
520
520
 
521
- Every process has a umask in the kernel, `022` for a process the embedder starts, inherited across fork, spawn and exec and shown in `/proc/<pid>/status` (`Umask:`). The kernel applies it when a program creates a file or directory, whatever its libc does: an Emscripten program's `open` with `O_CREAT`, `mkdir` and `mknod` (so `umask 077; touch f` gives `600` in a child too), and a WASI program's `path_open` with `O_CREAT` and `path_create_directory` (`0666` and `0777` less the umask). An Emscripten program's `umask()` sets and reads the kernel's umask through the glue's `__syscall_umask` import, so `umask 077` in bash shows in `/proc` and reaches every child. A build that resolves `umask` inside its wasm, as wasm-bash 5.3.0-8 to 5.3.0-10 do, keeps a umask of its own that the kernel never sees.
521
+ Every process has a umask in the kernel, `022` for a process the embedder starts, inherited across fork, spawn and exec and shown in `/proc/<pid>/status` (`Umask:`). The kernel applies it when a program creates a file or directory, whatever its libc does: an Emscripten program's `open` with `O_CREAT`, `mkdir` and `mknod` (so `umask 077; touch f` gives `600` in a child too), and a WASI program's `path_open` with `O_CREAT` and `path_create_directory` (`0666` and `0777` less the umask). An Emscripten program's `umask()` sets and reads the kernel's umask through the glue's `__syscall_umask` import, so `umask 077` in bash shows in `/proc` and reaches every child. The kernel finds these imports by the glue's own functions (`___syscall_openat`, `___syscall_mkdirat`, `___syscall_mknodat`, `___syscall_umask`, and `___syscall_faccessat` for `access(2)`), so it also does so for glue whose imports are minified to short names, as wasm-curl's and wasm-clang's are; a glue that has one of those functions but doesn't import it under any name is refused. A build that resolves `umask` inside its wasm, as wasm-bash 5.3.0-8 to 5.3.0-10 do, keeps a umask of its own that the kernel never sees.
522
522
 
523
523
  WASI and WASIX have no call for file modes, so the kernel adds the import module `slicc_fs`, whose functions return a preview1 errno (kernels without them answer `ENOSYS`):
524
524
 
@@ -590,9 +590,10 @@ With `users: 'enforce'` the kernel checks every file call that reaches it agains
590
590
  - **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.
591
591
  - **`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)).
592
592
  - **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).
593
- - **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. `/etc/sudoers` and `/etc/sudoers.d` are written when missing (see [Sudo](#sudo)).
593
+ - **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, and the first time owners are on for a root from before owners, what is in it without an owner of its own is removed (as a boot empties `/tmp` on Linux; a file that is open is kept, a symlinked `/tmp` is left alone, and one `console.warn` line says how many entries went), since it would otherwise be root's, and a program that made it for its user, such as pnpm's lock directories, would find it isn't its own. 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. `/etc/sudoers` and `/etc/sudoers.d` are written when missing (see [Sudo](#sudo)).
594
594
  - **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.
595
595
  - **A client's `fs`:** checked as the client's user; a page's client (root) passes.
596
+ - **Removed files still open:** a process that holds a removed file may look it up (`stat`, `lstat`) under the hidden name the kernel gave it in `/.slicc-unlinked`, which is root's `0700`; every other process is refused there, so the kernel's own bookkeeping of held files raises no denials for their holders.
596
597
 
597
598
  `users: 'enforce'` is not the default yet; it becomes so once the embedders have checked their programs under `'report'`.
598
599
 
@@ -603,7 +604,7 @@ With `users: 'enforce'` the kernel checks every file call that reaches it agains
603
604
  - **`sudo` elevates only under `users: 'enforce'`.** With `'off'` and `'report'`, `sudo command` just runs `command`, as the caller, with its environment. Under `'report'` nothing is refused, so anyone could rewrite the policy or a command; `sudo` grants nothing until the kernel enforces.
604
605
  - **The policy** is `/etc/sudoers` and the files in `/etc/sudoers.d/` (in name order, skipping names that start with `.` or end in `~`), which the kernel writes at start when they're missing, with owners on: `/etc/sudoers` root's `0440` with the one line `%sudo Cmnd *`, and `/etc/sudoers.d` root's `0750`. There is no API: an admin (bios, as root) edits `/etc/sudoers.d/<user>`. A line is `[NOPASSWD] <user|%group|ALL> [(<target>)] Cmnd <argv…|*>`: who (a user name, a group, or anyone), the user it may run as (`root` by default, or `ALL`), and the command, as shell-quoted words whose first is an absolute path, matched exactly against the resolved path and the arguments, or `*` for any command. `#` lines and blank lines are ignored. A line that doesn't parse is skipped, and so is a policy file that isn't root's, is a symlink, or is writable by group or others, or one in a directory that is (each logged once with `console.warn`): what's skipped grants nothing. Groups are the caller's process groups, as `id` shows them.
605
606
  - **The embedders' recipe** puts cones in `sudo` (`groups: ['sudo']`) and scoops not, so a cone may ask for any command and a scoop for none.
606
- - **The command** is found once, on `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` for a bare name or against the caller's directory for a path, and its real path (symlinks resolved) is what runs and what the policy sees. That file, and every file that will really run for it (a catalog command's glue, module and imports module, and the `package.json` that defines it, a script and the interpreter its `#!` line or `binfmt` suffix names), must be root's and writable by root only, and so must every directory above each, or `sudo` refuses (`EPERM`), whatever the policy says. Other files a program loads at run time (a package's libraries) are the admin's to keep safe, as on Linux. The caller's directory, which the program passes on, must be a directory the caller may enter, or `sudo` refuses (`EACCES`). The kernel resolves it to its real path once, shows that in the request, and runs the command there; if it no longer resolves the same, or the caller can no longer enter it, when the answer comes or when the command starts, nothing runs.
607
+ - **The command** is found once, on `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` for a bare name or against the caller's directory for a path, and its real path (symlinks resolved) is what runs and what the policy sees. That file, and every file that will really run for it (a catalog command's glue, module and imports module, and the `package.json` that defines it, a script and the interpreter its `#!` line or `binfmt` suffix names), must be root's and writable by root only, and so must every directory above each, or `sudo` refuses (`EPERM`), whatever the policy says. A command that wouldn't start as the target user (a `#!` line naming an interpreter that is only the caller's, say) fails with `command not found` before anything is asked. Other files a program loads at run time (a package's libraries) are the admin's to keep safe, as on Linux. The caller's directory, which the program passes on, must be a directory the caller may enter, or `sudo` refuses (`EACCES`). The kernel resolves it to its real path once, shows that in the request, and runs the command there; if it no longer resolves the same, or the caller can no longer enter it, when the answer comes or when the command starts, nothing runs.
607
608
  - **The decision:** a caller whose effective uid is 0 runs anything (the check on the command's path still applies). Otherwise a matching `NOPASSWD` line runs the command at once; any other matching line asks `approve`; nothing matching is refused (`sudo: cone is not allowed to run /usr/bin/ls as root`).
608
609
  - **Approval:** `approve(req, { signal })` gets `{ id, kind: 'sudo', user, uid, target, argv, path, cwd, pid, tag?, tty?, createdAt, expiresAt, always: true }`: the caller's name and real uid, the target user, the argv as typed, the resolved `path`, the caller's directory, pid, tag and terminal, and the times in ms. It answers `'once'` (run it), `'always'` (run it, and append `NOPASSWD <user> (<target>) Cmnd <path> <args…>` to `/etc/sudoers.d/<user>`, root's `0440`, so it's never asked again; if that can't be written, nothing runs) or `'deny'`; a throw, any other answer or no hook at all is `sudo: approval denied`. After 5 minutes the request expires (`signal` aborts with `'expired'`, and `sudo` fails with `sudo: the request expired`); when the requester ends or gets a signal while it waits, `^C` included, `signal` aborts with `'withdrawn'` and `sudo` ends with that signal's status. A user has at most one request waiting; another fails with `sudo: another request is pending`. On the page, `createKernel` relays the request from the kernel worker and back, and the abort too; `terminate()` or a failed worker aborts every request still waiting with `'withdrawn'`.
609
610
  - **Running:** the command runs as the caller's child, with all six ids and the groups of the target user, in the caller's directory, on its terminal, standard input, output and error, and in its process group; `sudo` exits with its status, and passes `SIGTERM` and `SIGHUP` on to it, also one that comes while it starts (the kernel lets it signal the command it started, which its own user may not); `SIGINT` and `SIGQUIT` it leaves to the command, which gets them from the terminal itself. It's found among the system's commands only, never a user's own. Its environment is the kernel's (the embedder's `env` and the proxy settings), the target's `HOME`, `USER`, `LOGNAME`, `SHELL`, `PNPM_HOME` and `NPM_CONFIG_PREFIX`, `PATH` set to the path above, `PWD`, the caller's `TERM` when it is a plain name (letters, digits, `.`, `_`, `+`, `-`), and `SUDO_USER`, `SUDO_UID`, `SUDO_GID` and `SUDO_COMMAND`; nothing else of the caller's comes through.
@@ -1307,7 +1307,7 @@ var OpfsFs = class {
1307
1307
  const above = owner && {
1308
1308
  uid: owner.uid,
1309
1309
  gid: owner.gid,
1310
- ...mode ? { mode, parents: mode } : {}
1310
+ ...mode !== void 0 ? { mode, parents: mode } : {}
1311
1311
  };
1312
1312
  await this.mkdir(parentOf3(path), { recursive: true, ...above ? { owner: above } : {} });
1313
1313
  }
@@ -3582,6 +3582,9 @@ var UserDb = class {
3582
3582
  const pnpm = `${home}/.local/share/pnpm`;
3583
3583
  if (await this.realDir(pnpm)) await this.fs.chmod(pnpm, scoop ? 488 : 448);
3584
3584
  }
3585
+ umaskFor(options) {
3586
+ return (options.umask ?? this.umasks.get(options.name) ?? 18) & 511;
3587
+ }
3585
3588
  async realDir(path) {
3586
3589
  const parts2 = path.split("/").filter(Boolean);
3587
3590
  for (let n = 1; n <= parts2.length; n++) {
@@ -3605,9 +3608,11 @@ var UserDb = class {
3605
3608
  group: parseGroup(await this.read(GROUP, DEFAULT_GROUP))
3606
3609
  };
3607
3610
  }
3608
- async provision(row) {
3609
- const owner = this.owners ? { uid: row.uid, gid: row.gid } : void 0;
3610
- const made2 = owner ? { recursive: true, owner } : { recursive: true };
3611
+ async provision(row, umask, shared) {
3612
+ const dirs = 511 & ~umask | (shared ? 1024 : 0);
3613
+ const gid = shared?.gid ?? row.gid;
3614
+ const owner = this.owners ? { uid: row.uid, gid } : void 0;
3615
+ const made2 = owner ? { recursive: true, owner: { ...owner, mode: dirs, parents: dirs } } : { recursive: true };
3611
3616
  const pnpm = `${row.home}/.local/share/pnpm`;
3612
3617
  if (!await this.fs.exists(pnpm)) {
3613
3618
  await this.fs.mkdir(pnpm, made2);
@@ -3616,7 +3621,7 @@ var UserDb = class {
3616
3621
  const config = `${row.home}/.config/pnpm/config.yaml`;
3617
3622
  if (!await this.fs.exists(config)) {
3618
3623
  await this.fs.mkdir(`${row.home}/.config/pnpm`, made2);
3619
- await this.fs.writeFile(config, PNPM_CONFIG, owner);
3624
+ await this.fs.writeFile(config, PNPM_CONFIG, owner && { ...owner, mode: 438 & ~umask });
3620
3625
  }
3621
3626
  }
3622
3627
  async commit(passwd, group, then) {
@@ -3707,7 +3712,7 @@ var UserDb = class {
3707
3712
  throw fsError("EINVAL", `a user is no member of its own group ${name2}`);
3708
3713
  }
3709
3714
  if (found.uid >= FIRST_UID && found.uid <= LAST_UID) {
3710
- await this.provision(found);
3715
+ await this.provision(found, this.umaskFor(options), shared);
3711
3716
  await this.stampHome(found, shared, homeMode);
3712
3717
  }
3713
3718
  if (joined(name2, more)) await this.fs.writeFile(GROUP, formatGroup(group));
@@ -3725,7 +3730,7 @@ var UserDb = class {
3725
3730
  const fresh2 = !await this.fs.exists(home);
3726
3731
  await this.fs.mkdir(home, { recursive: true });
3727
3732
  try {
3728
- await this.provision(row);
3733
+ await this.provision(row, this.umaskFor(options), shared);
3729
3734
  await this.stampHome(row, shared, homeMode);
3730
3735
  await this.fs.mkdir("/etc", { recursive: true });
3731
3736
  await this.commit(formatPasswd(passwd), formatGroup(group));
@@ -7832,6 +7837,11 @@ var HeldPaths = class extends Map {
7832
7837
  keptFor(handle) {
7833
7838
  return this.keptHandles.get(handle);
7834
7839
  }
7840
+ keeps(hidden) {
7841
+ for (const file of this.uses.keys()) if (file.hidden === hidden) return true;
7842
+ for (const file of this.unlinked.values()) if (file.hidden === hidden) return true;
7843
+ return false;
7844
+ }
7835
7845
  keep(path, file) {
7836
7846
  const fresh2 = this.handlesAt(path).filter((id2) => !this.keptHandles.has(id2));
7837
7847
  for (const id2 of fresh2) this.keptHandles.set(id2, file);
@@ -8933,6 +8943,7 @@ function guardOf(entry) {
8933
8943
  return { fs: entry.fs, cred, pid: entry.pid ?? 0, dac: entry.dac };
8934
8944
  }
8935
8945
  var FOLLOWING = /* @__PURE__ */ new Set(["exists", "stat", "chown", "statfs"]);
8946
+ var LOOKS = /* @__PURE__ */ new Set(["exists", "stat", "lstat"]);
8936
8947
  var NOT_FOLLOWING = /* @__PURE__ */ new Set(["lstat", "readlink", "lchown"]);
8937
8948
  async function guard(g, path, req, cwd) {
8938
8949
  const { op } = req;
@@ -9002,6 +9013,7 @@ async function accessOp(entry, path, req) {
9002
9013
  async function guarded(entry, path, req) {
9003
9014
  const g = guardOf(entry);
9004
9015
  if (!g) return void 0;
9016
+ if (LOOKS.has(req.op) && await entry.holdsKept?.(path)) return void 0;
9005
9017
  try {
9006
9018
  await guard(g, path, req, entry.cwd);
9007
9019
  return void 0;
@@ -12545,6 +12557,13 @@ var WasmProcess = class {
12545
12557
  }
12546
12558
  }
12547
12559
  keptPath;
12560
+ async holds(path) {
12561
+ for (const fd of this.fds.numbers()) {
12562
+ const { file } = this.fds.get(fd);
12563
+ if ((await file.stat?.().catch(() => void 0))?.path === path) return true;
12564
+ }
12565
+ return false;
12566
+ }
12548
12567
  keptHandle;
12549
12568
  async keptOr(path, handle) {
12550
12569
  const held = handle === void 0 ? void 0 : this.keptHandle?.(handle);
@@ -13188,7 +13207,8 @@ function spawnWasmProcess(opts) {
13188
13207
  const token = holdingToken(opts, holds, {
13189
13208
  cred: () => process.cred,
13190
13209
  umask: () => process.umask,
13191
- created: created3
13210
+ created: created3,
13211
+ holdsKept: async (path) => path.includes(`/${UNLINKED_DIR}/`) && (holds.keeps(path) || await process.holds(path))
13192
13212
  });
13193
13213
  process.keptPath = (path) => holds.keptAt(path)?.hidden;
13194
13214
  process.keptHandle = (handle2) => holds.keptFor(handle2)?.hidden;
@@ -15550,9 +15570,14 @@ var Sudo = class {
15550
15570
  if (walked.denied !== void 0 || walked.missing) return false;
15551
15571
  return (await this.o.fs.stat(cwd).catch(() => void 0))?.isDirectory === true;
15552
15572
  }
15553
- async untrusted(path, cwd) {
15554
- const files = await this.o.backing?.(path, cwd) ?? [];
15555
- for (const file of [path, ...files]) if (!await this.trusted(file)) return file;
15573
+ async untrusted(path, cwd, word) {
15574
+ const files = this.o.backing ? await this.o.backing(path, cwd) : [];
15575
+ if (!files) return failed2("ENOENT", `sudo: ${word}: command not found`);
15576
+ for (const file of [path, ...files]) {
15577
+ if (!await this.trusted(file)) {
15578
+ return failed2("EPERM", `sudo: ${file} must be root's, and writable by root only`);
15579
+ }
15580
+ }
15556
15581
  return void 0;
15557
15582
  }
15558
15583
  async locate(word, cwd) {
@@ -15593,10 +15618,8 @@ var Sudo = class {
15593
15618
  }
15594
15619
  const path = await this.locate(word, cwd);
15595
15620
  if (!path) return failed2("ENOENT", `sudo: ${word}: command not found`);
15596
- const untrusted = await this.untrusted(path, cwd);
15597
- if (untrusted) {
15598
- return failed2("EPERM", `sudo: ${untrusted} must be root's, and writable by root only`);
15599
- }
15621
+ const untrusted = await this.untrusted(path, cwd, word);
15622
+ if (untrusted) return untrusted;
15600
15623
  const argv = [path, ...args];
15601
15624
  const matching = rules.filter(
15602
15625
  (rule) => applies(rule) && (rule.target === "ALL" || rule.target === target.name) && (rule.cmnd === "*" || rule.cmnd.length === argv.length && rule.cmnd.every((w, i) => w === argv[i]))
@@ -15619,10 +15642,8 @@ var Sudo = class {
15619
15642
  if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
15620
15643
  return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
15621
15644
  }
15622
- const changed = await this.untrusted(path, cwd);
15623
- if (changed) {
15624
- return failed2("EPERM", `sudo: ${changed} must be root's, and writable by root only`);
15625
- }
15645
+ const changed = await this.untrusted(path, cwd, word);
15646
+ if (changed) return changed;
15626
15647
  if (answer === "always" && !await this.remember(self, target, argv)) {
15627
15648
  return failed2("EIO", "sudo: the rule could not be recorded");
15628
15649
  }
@@ -16252,7 +16273,7 @@ var Launcher = class {
16252
16273
  }
16253
16274
  async backing(file, cwd) {
16254
16275
  const planned = await this.plan(file, [file], cwd, "");
16255
- if (!planned) return [];
16276
+ if (!planned) return void 0;
16256
16277
  const { glue, wasm, imports } = planned.target;
16257
16278
  const files = [glue, wasm, ...imports ? [imports] : []];
16258
16279
  const scripts = [];
@@ -16860,6 +16881,27 @@ var Launcher = class {
16860
16881
  await this.provisionPnpmHome();
16861
16882
  return entry;
16862
16883
  }
16884
+ async ownTmp() {
16885
+ const real = await this.realLayoutPath("/tmp");
16886
+ const st = real === void 0 ? void 0 : await this.base.lstat(real).catch(() => void 0);
16887
+ if (real !== void 0 && st?.isDirectory && this.base.hasOwner && !await this.base.hasOwner(real)) {
16888
+ let removed = 0;
16889
+ for (const name2 of await this.base.readdir(real).catch(() => [])) {
16890
+ const path = joinName(real, name2);
16891
+ if (await this.base.hasOwner(path) || this.inUse(path)) continue;
16892
+ const failed3 = await this.base.rm(path, { recursive: true, force: true }).catch(String);
16893
+ if (failed3 === void 0) removed++;
16894
+ }
16895
+ if (removed > 0) {
16896
+ console.warn(`slicc-kernel users: removed ${removed} entries from /tmp that had no owner`);
16897
+ }
16898
+ await this.base.chown?.(real, { uid: 0, gid: 0, mode: 1023, follow: false });
16899
+ }
16900
+ if (st?.isDirectory) await this.base.chmod(real, 1023);
16901
+ }
16902
+ inUse(path) {
16903
+ return [...this.openFiles].some((nodes) => nodes.holds(path)) || heldUnder(this.held, path);
16904
+ }
16863
16905
  async stampLayout() {
16864
16906
  const layout = [
16865
16907
  ...LAYOUT,
@@ -16979,7 +17021,7 @@ var Launcher = class {
16979
17021
  async prepare() {
16980
17022
  await this.resolvePrefixes();
16981
17023
  for (const dir of SHARED_DIRS) await this.base.mkdir(dir, { recursive: true });
16982
- if (this.owners) await this.base.chmod("/tmp", 1023);
17024
+ if (this.owners) await this.ownTmp();
16983
17025
  await this.provisionPnpmHome();
16984
17026
  await this.users.ensure();
16985
17027
  if (this.owners) await this.stampLayout();