@ai-ecoverse/slicc-kernel 1.58.1 → 1.59.0

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
@@ -90,7 +90,7 @@ The user database, `/etc/passwd` and `/etc/group` (see [Users](#users)):
90
90
  | | |
91
91
  | --- | --- |
92
92
  | `mode` | the `users` option the kernel was created with |
93
- | `add({ name, home?, groups?, umask?, homeGroup? })` | adds a user and resolves with `{ name, uid, gid, home, shell, groups, umask? }`. It's idempotent: an existing name resolves with its entry and changes nothing but the remembered `umask`. 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 (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, and its mode is then `2750`. A name must match `[a-z_][a-z0-9_.-]*`; a group that doesn't exist, in `groups` or `homeGroup`, rejects with `ENOENT` |
93
+ | `add({ name, home?, groups?, umask?, homeGroup? })` | 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, groups and home, and only 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, and its mode is then `2750`. A name must match `[a-z_][a-z0-9_.-]*`; a group that doesn't exist, in `groups` or `homeGroup`, rejects with `ENOENT` |
94
94
  | `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` |
95
95
  | `list()` | every user, as `add` returns them |
96
96
 
@@ -157,7 +157,7 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
157
157
  { "slicc": { "abi": "wasi", "commands": { "rg": { "wasm": "bin/rg.wasm" }, "rustc": { "script": "bin/rustc" } } } }
158
158
  ```
159
159
 
160
- Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. Of two packages that define a name, the one whose name sorts first wins (compared as strings by UTF-16 code unit, so a scoped `@scope/name` comes before names that start with a letter, but after those that start with a digit), whatever order the file system lists them in. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. Commands also come from pnpm's global directory. Every process gets `PNPM_HOME=/home/.local/share/pnpm`, unless the embedder sets its own, and has `$PNPM_HOME/bin` on `PATH`. The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
160
+ Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. Of two packages that define a name, the one whose name sorts first wins (compared as strings by UTF-16 code unit, so a scoped `@scope/name` comes before names that start with a letter, but after those that start with a digit), whatever order the file system lists them in. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. Commands also come from pnpm's global directory. Every process gets a `PNPM_HOME` and has `$PNPM_HOME/bin` on `PATH`: root's is `/usr/local/share/pnpm` (unless the embedder sets its own), a user's `~/.local/share/pnpm` (see [Users](#users)). The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
161
161
 
162
162
  A package can also name a command that runs files by their suffix, as `binfmt_misc` does: `"slicc": { "binfmt": { ".jsh": "jsh" } }`. Executing a file that has no `#!` line and is not a wasm module, whose name ends in a mapped suffix, runs that command with the file's path as invoked and the arguments after it, as a `#!` line naming the command would; a `#!` line still wins. The longest matching suffix wins, the whole name is never a suffix (`.jsh` alone is not one), and of two packages that map one suffix, the one whose name sorts first wins, as for commands; the kernel warns once (`console.warn`) when another package maps that suffix to a different command. A suffix starts with a dot and the command must be a name the catalog resolves when the file runs.
163
163
 
@@ -378,11 +378,11 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
378
378
  - **Built in:** `tmpfs`, which lives in memory until unmounted, in 1 MiB chunks; `fsa`, a folder the user picks (see [Removable media](#removable-media-fsa)); and `hostfs`, a folder the local proxy exports (see [Host folders](#host-folders-hostfs)).
379
379
  - **Package drivers:** a package declares a type in `"slicc": { "filesystems": { "<type>": { "module": "<file>" } } }`, and the kernel starts that module in a worker of its own for each mount. The module is a single self-contained file: it can't import bare specifiers.
380
380
  - Its default export receives `{ fetch }`, the kernel's network transport. It returns `{ handlers, capabilities }`.
381
- - The handlers are path-based: `getattr`, `readdir`, `open`/`read`/`write`/`release` on handles, `mkdir`, `rmdir`, `unlink`, `rename`, and optionally `symlink`, `readlink`, `setattr` (`mode`, `mtime`, and `size` to truncate) and `statfs`.
381
+ - The handlers are path-based: `getattr`, `readdir`, `open`/`read`/`write`/`release` on handles, `mkdir`, `rmdir`, `unlink`, `rename`, and optionally `symlink`, `readlink`, `setattr` (`mode`, `mtime`, `size` to truncate, and `uid`/`gid`) and `statfs`.
382
382
  - An optional `mount({ source, options })` handler validates the source.
383
383
  - Errors carry a POSIX `code`.
384
384
  - `@ai-ecoverse/slicc-kernel/driver` exports the types and `fsError`.
385
- - **Capabilities:** `readonly`, `symlinks`, `links` (hard links: the driver answers `link(from, to)`, reports `nlink` in its attributes, and gives every attribute a stable `ino`, the same for all names of a file; a mount whose driver offers `links` but answers an attribute without `ino` loses them: `link` is `EPERM` there and `st_nlink` is 1), `chmod`, `ranges`, `maxIo` (the largest read or write per message), `maxFile`, `listingStats`, `sessions`, `linkTimes`, and `attrTtl`/`entryTtl` (in ms, default 1000). `sessions` promises that a handle opened for writing keeps its writes to itself until it is released, so the kernel keeps one open per file being written instead of one per write (the `fsa` driver has it). `linkTimes` promises that `setattr` with `mtime` on a symlink sets the link's own time. Without it, setting a link's own times (`touch -h`, `utimensat` with `AT_SYMLINK_NOFOLLOW`) fails with `EOPNOTSUPP` and never reaches the target. tmpfs has it; hostfs does not. Setting times through a link follows it in the kernel, so `setattr` gets the target. `ranges` (driver protocol 1.1) promises that a write lands in the file at once, with no copy of the whole file on `open` or `release`, and that `setattr` takes `size`.
385
+ - **Capabilities:** `readonly`, `symlinks`, `links` (hard links: the driver answers `link(from, to)`, reports `nlink` in its attributes, and gives every attribute a stable `ino`, the same for all names of a file; a mount whose driver offers `links` but answers an attribute without `ino` loses them: `link` is `EPERM` there and `st_nlink` is 1), `chmod`, `owners`, `ranges`, `maxIo` (the largest read or write per message), `maxFile`, `listingStats`, `sessions`, `linkTimes`, and `attrTtl`/`entryTtl` (in ms, default 1000). `sessions` promises that a handle opened for writing keeps its writes to itself until it is released, so the kernel keeps one open per file being written instead of one per write (the `fsa` driver has it). `linkTimes` promises that `setattr` with `mtime` on a symlink sets the link's own time. Without it, setting a link's own times (`touch -h`, `utimensat` with `AT_SYMLINK_NOFOLLOW`) fails with `EOPNOTSUPP` and never reaches the target. tmpfs has it; hostfs does not. Setting times through a link follows it in the kernel, so `setattr` gets the target. `ranges` (driver protocol 1.1) promises that a write lands in the file at once, with no copy of the whole file on `open` or `release`, and that `setattr` takes `size`. `owners` (driver protocol 1.2) promises that the driver stores owners: its attributes carry `uid` and `gid`, `open` with `create`, `mkdir` and `symlink` may carry the new path's `uid`, `gid` and `mode` (the `owner` of the open flags, the second or third handler argument), a new path without them takes its directory's owner, and `setattr` takes `uid` and `gid`. tmpfs has it.
386
386
  - **Caching:** the kernel caches attributes and listings for those TTLs, and a driver can push `invalidate` for changes made outside.
387
387
  - **File contents:** on a mount with `ranges`, as `tmpfs` is, and on the root file system when it is in OPFS (in a browser with sync access handles) or an in-memory root, a program reads and writes a file in 64 KiB pages: reads come from the driver 1 MiB at a time, and changed pages go back in 1 MiB runs once 8 MiB have changed, at `fsync`, and when the file is closed (from the kernel's own descriptors also 250 ms after a write). A file of any size the mount can hold works, with memory to spare for about one more copy of it. In OPFS a write opens a sync access handle for just that write. An `fsa` mount (a picked folder) has no sync access handles, and Chrome copies the whole file into a swap file for every writable stream, so it writes through one stream per open file: the first write back opens it, later runs go into it at their offsets, and it is committed, one copy of the file, when the last process writing the file closes it (or exits), before another process reads what was written, and before a rename, unlink or unmount. Until then other processes see the new size but read the old bytes outside what was written. Elsewhere (`hostfs` with an older proxy, and a root outside OPFS), file contents are read whole when a program opens a file and written back when it closes it. Before a program's next call into the kernel (any wait, read of a pipe or terminal, sleep or spawn), its changed pages also go back, so other processes and the embedder see a write as soon as the writer does anything but compute: a daemon's pidfile, a log another process follows. On a whole-file mount that holds only for files of up to 64 KiB; a larger one goes back at close, fsync and exit, as before. Beyond that, both give close-to-open consistency, as NFS has. A process that ends without closing its files keeps what it wrote: on exit, on a trap, and when a signal kills it. For a killed WASI program, the kernel lets its worker write its changed pages back before `waitpid` reports the death. The only exception is a worker that makes no call into the kernel for 2 seconds after the signal; it is stopped without that write-back.
388
388
  - **Rules on a mount:**
@@ -478,6 +478,14 @@ WASI and WASIX have no call for file modes, so the kernel adds the import module
478
478
  | `umask(mask, old_out)` | sets the process's umask and writes the previous one |
479
479
  | `fd_mode(fd, mode_out)` | the permission bits of an open file or directory |
480
480
  | `path_mode(dirfd, path, path_len, flags, mode_out)` | those of a path, of the link itself with `flags & 1` |
481
+ | `fd_chown(fd, uid, gid)` | `fchown` on an open file or directory; `-1` keeps an id |
482
+ | `path_chown(dirfd, path, path_len, uid, gid, flags)` | `fchownat`, of the link itself with `flags & 1` |
483
+ | `fd_owner(fd, out)` | writes the owner of an open file as `u32` uid, then `u32` gid |
484
+ | `path_owner(dirfd, path, path_len, flags, out)` | those of a path, of the link itself with `flags & 1` |
485
+ | `fd_filestat_owner(fd, filestat_out, out)` | `fd_filestat_get` and `fd_owner` in one call |
486
+ | `path_filestat_owner(dirfd, lookupflags, path, path_len, filestat_out, out)` | `path_filestat_get` (with its preview1 lookup flags: `1` follows a symlink) and the owner of the same path, in one call, so a libc's `stat` costs one call either way |
487
+
488
+ The owner calls follow [File owners](#file-owners): with `users: 'off'`, `fd_chown` and `path_chown` succeed and change nothing, and the four owner calls answer `ENOTSUP`, "owners not tracked", before looking at anything, so a libc can remember that once and take its own `getuid()` and `getgid()` as every file's owner, which is what an Emscripten program built against the credential calls sees. Devices are root's. A pipe's owner is the caller.
481
489
 
482
490
  WASIX's own terminal calls carry only echo and line buffering (`tty_set`) and answer for "the" terminal whatever the descriptor (`tty_get`), so a WASIX program's `cfmakeraw` would leave `^C` a signal and its output post-processed. The kernel adds the import module `slicc_tty`, with the full Linux `struct termios` (musl's 60-byte layout: four `u32` flag words, `c_line`, `c_cc[32]`, then input and output speed; speeds read as `B38400` and are ignored when set) and answers per descriptor, again as a preview1 errno (`ENOTTY` for a descriptor that is no terminal, `EBADF` for one that isn't open, `ENOSYS` from kernels without them):
483
491
 
@@ -512,11 +520,11 @@ Programs ask the kernel for their ids:
512
520
  With `createKernel({ users: 'report' })` or `'enforce'`, files have owners. Nothing checks them against a process's credentials yet; `'report'` and `'enforce'` differ once permissions are checked. With `'off'`, the default, nothing below happens: the kernel stores no owner, and every file looks owned by the caller as described under [Metadata](#metadata).
513
521
 
514
522
  - **Where owners live:** on the root file system, in the sidecar record's `uid` and `gid` (see [Metadata](#metadata)). A path with no owner of its own has the owner of its nearest ancestor that has one, and `/` belongs to root, so a root written before owners existed, or by the BIOS through the OPFS API, is root's throughout. A directory whose owner changes keeps the owner its unowned contents had, in `heir`, so `chown cone /old` gives `cone` the directory and leaves the files below it to their old owner. A rename keeps the owner of what it moves.
515
- - **New files:** a file, directory or symlink a process creates belongs to its effective uid, and to its effective gid, or to the directory's group when the directory has the setgid bit; a new directory in a setgid directory gets the setgid bit too. Every file and directory a process creates gets the mode it asked for (`0666` and `0777` when it can't ask, as a JS program's `ctx.fs` can't) less the process's umask, whichever runtime made it; an Emscripten program's file no longer gets `0644` whatever it asked for. A file written through a client's `fs` belongs to the client's user (root for a page) and gets `0644`, a directory `0755`; the kernel's own files, such as `/etc/passwd`, have no owner of their own.
516
- - **Homes:** every `kernel.users.add` gives the user's home (only a real directory at `/home/<dir>`, not a symlink or a path through one) and everything in it without an owner of its own to the user, and sets the home's mode: `0700`, `0750` for a `scoop-` user, `2750` with `homeGroup`. The embedder calls `add` for each user at boot, as it already does, so homes made before owners were on become the user's. That also means a user's own `chmod` or `chgrp` of its home lasts only until the next `add`, and `add` must get the same `homeGroup` every time.
517
- - **`chown`, `lchown`, `fchown`:** Emscripten programs change owners through the kernel, by the Linux rules: root may set any owner and group; a file's owner may set its group to one it belongs to, or leave it as it is; anything else fails with `EPERM`. `-1` keeps an id. A `chown` that sets an owner or group of a file clears its setuid bit, and its setgid bit if the file is group-executable, whoever calls it, root included, as on Linux. With `users: 'off'`, `chown` succeeds and changes nothing, as before.
518
- - **What programs see:** `stat` shows the owners to Emscripten programs, whatever their libc reports for their own ids, so a program built before H1 sees real owners too, and its owner checks can fail.
519
- - **Mounts:** `tmpfs`, `hostfs` and `fsa` mounts don't keep owners yet. Their files show as root's, and `chown` on them fails with `EPERM`.
523
+ - **New files:** a file, directory or symlink a process creates belongs to its effective uid, and to its effective gid, or to the directory's group when the directory has the setgid bit; a new directory in a setgid directory gets the setgid bit too. Every file and directory a process creates gets the mode it asked for (`0666` and `0777` when it can't ask, as a JS program's `ctx.fs` can't) less the process's umask, whichever runtime made it; an Emscripten program's file no longer gets `0644` whatever it asked for. A file written through a client's `fs` belongs to the client's user (root for a page) and gets `0666`, a directory `0777`, less the umask `kernel.users.add` gave that user (`022` for root and by default); the kernel's own files, such as `/etc/passwd`, have no owner of their own.
524
+ - **Homes:** every `kernel.users.add` gives the user's home (only a real directory at `/home/<dir>`, not a symlink or a path through one) and everything in it without an owner of its own to the user, and sets the home's mode: `0700`, `0750` for a `scoop-` user, `2750` with `homeGroup`, and its `~/.local/share/pnpm`'s to `0700` (`0750` for a `scoop-` user). The embedder calls `add` for each user at boot, as it already does, so homes made before owners were on become the user's. That also means a user's own `chmod` or `chgrp` of its home lasts only until the next `add`, and `add` must get the same `homeGroup` every time.
525
+ - **`chown`, `lchown`, `fchown`:** Emscripten programs, and WASI programs through `slicc_fs` (see [File modes and the umask](#file-modes-and-the-umask)), change owners through the kernel, by the Linux rules: root may set any owner and group; a file's owner may set its group to one it belongs to, or leave it as it is; anything else fails with `EPERM`. `-1` keeps an id. A `chown` that sets an owner or group of a file clears its setuid bit, and its setgid bit if the file is group-executable, whoever calls it, root included, as on Linux. With `users: 'off'`, `chown` succeeds and changes nothing, as before.
526
+ - **What programs see:** `stat` shows the owners to Emscripten programs, whatever their libc reports for their own ids, so a program built before H1 sees real owners too, and its owner checks can fail. WASI programs read them with `slicc_fs.fd_owner` and `path_owner`. The commands in `/usr/bin` and `/bin` are root's, and a user's own commands in `~/.local/share/pnpm/bin` belong to the owner of the closest real directory above them, normally the user.
527
+ - **Mounts:** the options `uid=` and `gid=` (decimal ids) give a mount its owner; without them it belongs to whoever mounted it: root for `kernel.mount`, `/etc/fstab` and a client, the process's effective ids for `mount(2)`, which then shows them in its options. A mount whose driver keeps owners (`tmpfs`) starts with its root owned so, and its files get owners as on the root file system, `chown` included. On `hostfs`, `fsa` and any driver without `owners`, every file shows as the mount's owner and `chown` fails with `EPERM`, root's too, as on a FAT drive. An embedder that mounts a folder for a user (bios's linked and picked folders) passes that user's `uid` and `gid`, or the user's programs see root's files there, and git calls the repository's ownership dubious.
520
528
 
521
529
  ### `/proc`
522
530
 
@@ -896,7 +896,7 @@ async function attachKernel(port, options = {}) {
896
896
  }
897
897
 
898
898
  // src/mount/protocol.ts
899
- var DRIVER_PROTOCOL = [1, 1];
899
+ var DRIVER_PROTOCOL = [1, 2];
900
900
  function driverVersionError(theirs) {
901
901
  const major = Array.isArray(theirs) ? theirs[0] : void 0;
902
902
  if (major === DRIVER_PROTOCOL[0]) return void 0;
@@ -905,6 +905,17 @@ function driverVersionError(theirs) {
905
905
  }
906
906
 
907
907
  // src/mount/driver.ts
908
+ function ownerOf(req) {
909
+ const owner = {
910
+ ...req.uid !== void 0 ? { uid: req.uid } : {},
911
+ ...req.gid !== void 0 ? { gid: req.gid } : {},
912
+ ...req.mode !== void 0 ? { mode: req.mode } : {}
913
+ };
914
+ return Object.keys(owner).length > 0 ? owner : void 0;
915
+ }
916
+ function withOwner(base, owner) {
917
+ return owner ? { ...base, owner } : base;
918
+ }
908
919
  function fsError(code, message = code) {
909
920
  return Object.assign(new Error(message), { code });
910
921
  }
@@ -915,13 +926,19 @@ async function dispatch(h, req) {
915
926
  case "readdir":
916
927
  return h.readdir(req.path);
917
928
  case "open":
918
- return h.open(req.path, {
919
- write: req.write,
920
- create: req.create,
921
- truncate: req.truncate,
922
- exclusive: req.exclusive,
923
- ...req.ifMatch !== void 0 ? { ifMatch: req.ifMatch } : {}
924
- });
929
+ return h.open(
930
+ req.path,
931
+ withOwner(
932
+ {
933
+ write: req.write,
934
+ create: req.create,
935
+ truncate: req.truncate,
936
+ exclusive: req.exclusive,
937
+ ...req.ifMatch !== void 0 ? { ifMatch: req.ifMatch } : {}
938
+ },
939
+ ownerOf(req)
940
+ )
941
+ );
925
942
  case "read":
926
943
  return h.read(req.fh, req.offset, req.size);
927
944
  case "write":
@@ -929,7 +946,7 @@ async function dispatch(h, req) {
929
946
  case "release":
930
947
  return h.release(req.fh);
931
948
  case "mkdir":
932
- return h.mkdir(req.path);
949
+ return ownerOf(req) ? h.mkdir(req.path, ownerOf(req)) : h.mkdir(req.path);
933
950
  case "rmdir":
934
951
  return h.rmdir(req.path);
935
952
  case "unlink":
@@ -941,7 +958,7 @@ async function dispatch(h, req) {
941
958
  return h.link(req.from, req.to);
942
959
  case "symlink":
943
960
  if (!h.symlink) throw fsError("EPERM", "this file system has no symlinks");
944
- return h.symlink(req.target, req.path);
961
+ return ownerOf(req) ? h.symlink(req.target, req.path, ownerOf(req)) : h.symlink(req.target, req.path);
945
962
  case "readlink":
946
963
  if (!h.readlink) throw fsError("EINVAL", `${req.path} is not a symlink`);
947
964
  return h.readlink(req.path);
@@ -949,7 +966,9 @@ async function dispatch(h, req) {
949
966
  return h.setattr?.(req.path, {
950
967
  ...req.mode !== void 0 ? { mode: req.mode } : {},
951
968
  ...req.mtime !== void 0 ? { mtime: req.mtime } : {},
952
- ...req.size !== void 0 ? { size: req.size } : {}
969
+ ...req.size !== void 0 ? { size: req.size } : {},
970
+ ...req.uid !== void 0 ? { uid: req.uid } : {},
971
+ ...req.gid !== void 0 ? { gid: req.gid } : {}
953
972
  });
954
973
  case "statfs":
955
974
  return h.statfs?.() ?? null;