@ai-ecoverse/slicc-kernel 1.133.4 → 1.133.5

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
@@ -846,6 +846,24 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
846
846
  - **Errors** are Linux's: `ENODEV` for an unknown type (or `hostfs` without a hook), `ENOENT` and `ENOTDIR` for the target, `EBUSY` for a target that is mounted already, and `EINVAL` for unmounting what is not a mount point.
847
847
  - **`/proc/mounts`** escapes spaces and backslashes as Linux does, and adds `nomedium` or `failed` to the options of a mount in that state.
848
848
 
849
+ ### The working directory
850
+
851
+ A process's working directory is the directory it changed into, not just its path ([#1164](https://github.com/ai-ecoverse/slicc-kernel/issues/1164)). The kernel records the directory's inode when a process starts and at every `chdir`. A forked child shares its parent's, and so does a spawned child unless the spawn changes directory.
852
+ - **When the directory is replaced:** if it's removed, or something else takes its name (`rm -r d; mkdir d`, `rm -r d; mv n d`, or `mv d x; mv n d`, from this kernel or from another tab on the same root), the process keeps the removed directory:
853
+ - a wasip1 program's `.` (its `.` preopen) can't be opened or listed (`ENOENT`, an empty listing for its preopen descriptor);
854
+ - its relative lookups and creates are `ENOENT`;
855
+ - `..` still reaches the parent.
856
+ - **Detection costs nothing in the steady state:**
857
+ - each held directory has a counter in memory the kernel shares with the process. It moves only when this kernel, another tab, or a `postFsNotice` reports a change at that directory or above it: the kernel's own watcher paths, and every path and tree of a peer change or notice.
858
+ - a process checks the counter, a memory read, before each relative lookup. Only after it moved does it ask the kernel once (`cwd-check`), and the kernel compares the inode at the path with the one it recorded.
859
+ - **The `rm` record:** so that `rm -r d; mkdir d` can be told apart, removing a directory leaves the same small record a rename leaves at the name it vacates (see [Metadata](#metadata)).
860
+ - **Limits:**
861
+ - a WASIX program's C library keeps the working directory as a string and turns relative paths into absolute ones before they reach the kernel. So a WASIX program still sees the new directory through relative paths, and `getcwd` still returns the path, until wasix-sysroot resolves relative paths against the working directory's descriptor ([#1164](https://github.com/ai-ecoverse/slicc-kernel/issues/1164) stays open for that).
862
+ - Emscripten and JS programs: [#1164](https://github.com/ai-ecoverse/slicc-kernel/issues/1164), next.
863
+ - a removal between the check and the lookup is a race, as with any path, and the lookup sees the new directory.
864
+ - on a `hostfs` folder the inode is the host's, which a host file system may reuse for the new directory (ext4 can; APFS doesn't). There the replacement isn't noticed.
865
+ - with more than 1023 distinct working directories held at once, the rest are not tracked.
866
+
849
867
  ### Exec
850
868
 
851
869
  An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux, whether the descriptors live in the kernel or a program opened the file in its own worker: every open, read-only ones included, is known to the kernel, and all holders share the one file (writes through one are seen through the others). The kernel does not copy it: as NFS does with its "silly rename", it moves the file into a hidden directory, `.slicc-unlinked/`, at the root of the mount it lives on (or of the root file system), and serves the holders' reads and writes from there (a worker's I/O on the old path while the path is gone). No file data is held in memory for it. The file is deleted when the last holder closes it or exits, and the directory goes as soon as it is empty. A file created at the path afterwards is the new file for everyone, also for the holder; a holder whose file another process renames (rather than removes) still uses the old path. A rename over a held file stays atomic for everyone else, as `rename(2)` is: moving the held file aside and the new one into its place are two steps, so a lookup of that name (`open`, `stat`, a read) that comes while they run waits for both, and one that started before them and missed the name in between looks again (also when it spelled the name through a symlinked parent, which the wait doesn't match by path: a miss is retried whenever any replacement finished meanwhile, up to three tries). Every open finds the old file or the new one, never no file (#903). A read through a held descriptor and the rename take the file's turn in order: lookups start to wait for the rename only once it has its turn on every held file it moves aside, so a read that came first finishes, and a rename over a file other processes keep reading never stalls them or itself (#980). A process that opens the file while the rename waits for its turn is a holder too: the rename looks for holders again once it has its turn, so that process keeps the old file as well, and a holder that closed meanwhile leaves nothing to move aside. The same holds for a directory that is removed or renamed over while programs hold files in it: the removal keeps looking for holders until nothing new appears, moving each held file aside, and lookups inside the directory wait for it. An open, a hand-over of a program's descriptor to the kernel (at a fork) or a hold that was under way while the directory went looks its path up again, so it gets the kept old file or ENOENT, never an empty file at the old name (#1013). An open of a path that is gone, with a handle the process no longer holds, is ENOENT rather than a descriptor for a missing file. The OPFS root does the same for its own two steps: a rename onto an existing name replaces the file with `move()` (or, in a browser without it, a copy into the name and a removal of the source), and a lookup that loses that race to a stale handle looks again. A `tmpfs` mount replaces the name in one step; on `fsa` and `hostfs` mounts it is the real file system's `rename`. Listings in the kernel leave `.slicc-unlinked` out, but on an `fsa` or `hostfs` mount it is visible on the real machine while a removed file is held (add `.slicc-unlinked/` to a `.gitignore` there). Each kernel names its entries after itself and holds a Web Lock for that name while it runs; when a mount is set up (and the root at start), the kernel removes the entries whose owner holds no lock, so a tab that died while holding a file leaves nothing behind for long, and entries of other live tabs on the same origin are left alone. Under the Node entry, which has no Web Locks, entries are named after the kernel's process id instead, and an entry is removed when that process is gone (`kill(pid, 0)` fails with `ESRCH`); a reused pid leaves the entry alone. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
@@ -1087,7 +1105,7 @@ OPFS stores names, bytes, sizes and modification times, nothing else. Everything
1087
1105
 
1088
1106
  - mode bits (`chmod`; a `chmod` to the mode a file or directory already has writes nothing and leaves its ctime as it was, which saves a sidecar write for every installer that sets the mode a file was made with);
1089
1107
  - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again: any write or truncate moves it to the time of the write, also in Chrome, whose OPFS doesn't move a file's `lastModified` for writes through a sync access handle, so the kernel records the write's time itself, and it reaches the metadata store up to 100 ms after the write, so a tab closed inside that window keeps the old mtime on disk; the time follows a rename and is dropped with the file); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
1090
- - inode numbers that stay stable across renames. A file with no record has an inode derived from its path, and a rename keeps it on the file; the rename leaves a small record at the name it vacated with a fresh inode, so a file, symlink or directory made there later doesn't share the renamed one's ([#954](https://github.com/ai-ecoverse/slicc-kernel/issues/954)). An exclusive create doesn't count that record as a taken name, and the boot reconcile keeps it only while a live record still carries the inode it guards;
1108
+ - inode numbers that stay stable across renames. A file with no record has an inode derived from its path, and a rename keeps it on the file; the rename leaves a small record at the name it vacated with a fresh inode, so a file, symlink or directory made there later doesn't share the renamed one's ([#954](https://github.com/ai-ecoverse/slicc-kernel/issues/954)). Removing a directory leaves the same record, so a directory made at its name has a new inode, which is how a process's [working directory](#the-working-directory) notices the swap. The record becomes the new entry's own when something is created there; one with nothing at its name is pruned by the next boot that's alone on the root. An exclusive create doesn't count that record as a taken name, and the boot reconcile keeps it only while a live record still carries the inode it guards;
1091
1109
  - symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar, with their own times: `touch -h`, `lutimes` and `utimensat` with `AT_SYMLINK_NOFOLLOW` set the link's, never the target's. A link's own mode can't be changed, as on Linux: `chmod -h`, `lchmod` and `fchmodat` with `AT_SYMLINK_NOFOLLOW` fail with `EOPNOTSUPP` (which `chmod -h` and `tar` ignore for links) and never follow it, and `open` with `O_PATH | O_NOFOLLOW` gives a descriptor for the link itself.
1092
1110
 
1093
1111
  OPFS stays authoritative: an entry is consulted only for a path that exists in OPFS (symlinks only for paths that don't), so files written straight to OPFS, for example by the BIOS installer or a page, get the defaults: directories `0755`, files `0644`, and files under `node_modules/*/bin/` `0755`. A file deleted through the OPFS API disappears at once; its leftover entry is ignored and removed when the next kernel starts. Changes go to OPFS first and then to the sidecar, so a crash in between at worst loses the metadata of that one change. Each change is a read-modify-write inside one IndexedDB transaction, so two kernels (two tabs) on the same origin don't lose each other's updates.
@@ -1235,6 +1253,12 @@ Not part of the run yet: the rest of bios's setup (the `rest` shard, the upgrade
1235
1253
 
1236
1254
  The WASIX fixtures of `test/integration/epoll.test.mjs` (#674), under `test/integration/fixtures/`, are committed with their sources, build scripts and built wasm.
1237
1255
 
1256
+ ### `heldcwd`
1257
+
1258
+ `test/integration/fixtures/heldcwd`: a Rust wasm32-wasip1 program, built by `build.sh` with rustup's wasm32-wasip1 target, for the [working directory](#the-working-directory) rows (#1164).
1259
+ - `heldcwd`: lists `.`, waits for a line on stdin, then lists `.` again, tries a relative create and stat, and lists `..`.
1260
+ - `heldcwd loop N`: stats `old` N times, reads a line, then stats it N more times, for the count of `cwd-check` calls.
1261
+
1238
1262
  ### `tokiotest`
1239
1263
 
1240
1264
  A small tokio program built for WASIX (`wasm32-wasmer-wasi`), the fixture for `test/integration/epoll.test.mjs` (#674). It runs on tokio's current-thread runtime, or on the multi-thread one (2 workers) with `--multi` first:
@@ -2307,6 +2307,9 @@ var OpfsFs = class {
2307
2307
  }
2308
2308
  }
2309
2309
  await this.meta.remove([found.path]);
2310
+ if (found.handle?.kind === "directory") {
2311
+ await this.meta.update(found.path, (entry) => entry ?? vacatedAt(found.path));
2312
+ }
2310
2313
  this.touch(Date.now(), parentOf3(found.path));
2311
2314
  }
2312
2315
  links() {
@@ -7092,7 +7095,7 @@ function satisfies(version, range2) {
7092
7095
  }
7093
7096
 
7094
7097
  // src/version.ts
7095
- var KERNEL_VERSION = true ? "1.133.4" : "0.0.0-dev";
7098
+ var KERNEL_VERSION = true ? "1.133.5" : "0.0.0-dev";
7096
7099
  var OS_RELEASE = "6.0.0-slicc";
7097
7100
  function procVersion() {
7098
7101
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -10785,6 +10788,88 @@ var FifoTable = class {
10785
10788
  }
10786
10789
  };
10787
10790
 
10791
+ // src/kernel/held-cwd.ts
10792
+ var CWD_SLOTS = 1024;
10793
+ var UNTRACKED = CWD_SLOTS - 1;
10794
+ var under4 = (path, changed) => changed === "/" || path === changed || path.startsWith(`${changed}/`);
10795
+ var CwdTable = class {
10796
+ sab = new SharedArrayBuffer(CWD_SLOTS * 4);
10797
+ view = new Int32Array(this.sab);
10798
+ held = /* @__PURE__ */ new Map();
10799
+ free = [];
10800
+ next = 0;
10801
+ checks = 0;
10802
+ inodeOf;
10803
+ constructor(inodeOf2) {
10804
+ this.inodeOf = inodeOf2;
10805
+ }
10806
+ async hold(path, from) {
10807
+ const shared = from === void 0 ? void 0 : this.held.get(from);
10808
+ if (shared && shared.path === path) return this.share(from);
10809
+ const ino = await this.inodeOf(path).catch(() => void 0);
10810
+ const slot2 = this.free.pop() ?? (this.next < UNTRACKED ? this.next++ : UNTRACKED);
10811
+ if (slot2 === UNTRACKED) return slot2;
10812
+ this.held.set(slot2, {
10813
+ path,
10814
+ ino,
10815
+ refs: 1,
10816
+ dead: false,
10817
+ verified: Atomics.load(this.view, slot2)
10818
+ });
10819
+ return slot2;
10820
+ }
10821
+ share(slot2) {
10822
+ const held = slot2 === void 0 ? void 0 : this.held.get(slot2);
10823
+ if (!held) return UNTRACKED;
10824
+ held.refs++;
10825
+ return slot2;
10826
+ }
10827
+ release(slot2) {
10828
+ const held = slot2 === void 0 ? void 0 : this.held.get(slot2);
10829
+ if (!held || --held.refs > 0) return;
10830
+ this.held.delete(slot2);
10831
+ Atomics.add(this.view, slot2, 1);
10832
+ this.free.push(slot2);
10833
+ }
10834
+ stateOf(slot2) {
10835
+ const held = this.held.get(slot2);
10836
+ return { seen: held?.verified ?? Atomics.load(this.view, slot2), dead: held?.dead === true };
10837
+ }
10838
+ touched(paths, trees = []) {
10839
+ if (this.held.size === 0) return;
10840
+ for (const [slot2, held] of this.held) {
10841
+ if (held.dead) continue;
10842
+ if (paths.some((p) => under4(held.path, p)) || trees.some((t) => under4(held.path, t))) {
10843
+ Atomics.add(this.view, slot2, 1);
10844
+ }
10845
+ }
10846
+ }
10847
+ async check(slot2) {
10848
+ this.checks++;
10849
+ const gen = Atomics.load(this.view, slot2);
10850
+ const held = this.held.get(slot2);
10851
+ if (!held) return { gen, alive: true };
10852
+ if (!held.dead) {
10853
+ const ino = await this.inodeOf(held.path).catch(() => null);
10854
+ held.dead = ino === null || held.ino !== void 0 && ino !== held.ino;
10855
+ held.verified = gen;
10856
+ }
10857
+ return { gen, alive: !held.dead };
10858
+ }
10859
+ };
10860
+ function heldInit(o) {
10861
+ if (!o.cwds || o.cwdSlot === void 0) return {};
10862
+ const state = o.cwdState ?? { seen: 0, dead: false };
10863
+ return { held: { sab: o.cwds, slot: o.cwdSlot, ...state } };
10864
+ }
10865
+ function heldHooks(o) {
10866
+ if (!o.cwdCheck || !o.cwdChdir) return {};
10867
+ return { cwdCheck: o.cwdCheck, cwdChdir: o.cwdChdir };
10868
+ }
10869
+ function inheritedCwd(chdir, parent) {
10870
+ return chdir ? void 0 : parent;
10871
+ }
10872
+
10788
10873
  // src/fs/copy.ts
10789
10874
  async function present2(fs, path) {
10790
10875
  return fs.lstat(path).then(
@@ -21142,6 +21227,8 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
21142
21227
  "fd-list",
21143
21228
  "proc-alarm",
21144
21229
  "proc-umask",
21230
+ "cwd-check",
21231
+ "cwd-chdir",
21145
21232
  "proc-cred",
21146
21233
  "proc-setcred",
21147
21234
  "dl-log",
@@ -22467,6 +22554,14 @@ var WasmProcess = class {
22467
22554
  return { ok: true, kind: "void" };
22468
22555
  case "proc-umask":
22469
22556
  return { ok: true, kind: "json", json: this.setUmask(req.mask) };
22557
+ case "cwd-check":
22558
+ return {
22559
+ ok: true,
22560
+ kind: "json",
22561
+ json: await this.options.cwdCheck?.() ?? { gen: 0, alive: true }
22562
+ };
22563
+ case "cwd-chdir":
22564
+ return { ok: true, kind: "json", json: await this.options.cwdChdir?.(req.path) ?? {} };
22470
22565
  case "proc-cred":
22471
22566
  return { ok: true, kind: "json", json: copyCred(this.cred) };
22472
22567
  case "proc-setcred":
@@ -23016,6 +23111,7 @@ function processInit(opts, sab, umask, cred, lane) {
23016
23111
  umask,
23017
23112
  cred,
23018
23113
  ...opts.changes ? { changes: opts.changes } : {},
23114
+ ...heldInit(opts),
23019
23115
  ...opts.owners ? { owners: true } : {},
23020
23116
  ...opts.links !== void 0 ? { links: true } : {},
23021
23117
  ...opts.ppid !== void 0 ? { ppid: opts.ppid } : {}
@@ -23089,6 +23185,7 @@ function processOptions(opts, header3, hooks, created4) {
23089
23185
  ...opts.identity ? { identity: opts.identity } : {},
23090
23186
  ...opts.onSyscall ? { onSyscall: opts.onSyscall } : {},
23091
23187
  ...opts.settling ? { settling: opts.settling } : {},
23188
+ ...heldHooks(opts),
23092
23189
  spawner: opts.spawner,
23093
23190
  forker: opts.forker,
23094
23191
  ...opts.fork?.dl ? { dl: opts.fork.dl } : {},
@@ -23139,6 +23236,7 @@ function threadInit(opts, program, sab, cred, thread, main) {
23139
23236
  owners: opts.owners === true,
23140
23237
  ...opts.links !== void 0 ? { links: true } : {},
23141
23238
  ...opts.changes ? { changes: opts.changes } : {},
23239
+ ...heldInit(opts),
23142
23240
  ...opts.ppid !== void 0 ? { ppid: opts.ppid } : {},
23143
23241
  thread,
23144
23242
  signals: main
@@ -26770,6 +26868,11 @@ var Launcher = class {
26770
26868
  hostname;
26771
26869
  watchers = new FsWatchers();
26772
26870
  changes = new FileChanges();
26871
+ cwds = new CwdTable(async (path) => {
26872
+ const { dev, ino } = await this.base.stat(path);
26873
+ return `${dev ?? 0}:${ino}`;
26874
+ });
26875
+ cwdSlots = /* @__PURE__ */ new Map();
26773
26876
  pnpmHome;
26774
26877
  pnpmChain;
26775
26878
  pnpmMade;
@@ -26826,7 +26929,10 @@ var Launcher = class {
26826
26929
  this.sealed.tickets.normalise = (path) => physicalPathAsync(this.base, path);
26827
26930
  this.sealed.userOf = async (uid) => (await this.users.lookup(uid))?.name;
26828
26931
  if (this.owners) options.fs.trackOwners?.();
26829
- this.watchers.tap((paths) => this.changes.publish(...paths));
26932
+ this.watchers.tap((paths) => {
26933
+ this.changes.publish(...paths);
26934
+ this.cwds.touched(paths);
26935
+ });
26830
26936
  this.watchers.onReshape(() => {
26831
26937
  for (const nodes of this.openFiles) nodes.reshaped();
26832
26938
  });
@@ -26896,6 +27002,7 @@ var Launcher = class {
26896
27002
  options.fs.onPeer?.(() => this.flushOpen());
26897
27003
  options.fs.remoteChanges?.((change) => {
26898
27004
  this.changes.flush();
27005
+ this.cwds.touched(change.paths, change.trees);
26899
27006
  this.rewritten(change.paths, change.trees);
26900
27007
  });
26901
27008
  const holders = [this.held, this.openFiles];
@@ -27678,11 +27785,12 @@ var Launcher = class {
27678
27785
  }
27679
27786
  const done5 = this.heldUrl(program);
27680
27787
  try {
27788
+ const cwdSlot = await this.cwds.hold(cwd, req.cwdFrom);
27681
27789
  const handle = await this.admitted(
27682
27790
  program.abi !== "js",
27683
27791
  req.fds,
27684
- (fds, quietOom) => this.start({ ...req, fds, quietOom, cwd, program, argv0: target.argv0, args, env })
27685
- );
27792
+ (fds, quietOom) => this.start({ ...req, fds, quietOom, cwd, program, argv0: target.argv0, args, env, cwdSlot })
27793
+ ).finally(() => this.cwds.release(cwdSlot));
27686
27794
  void handle.exited.finally(done5);
27687
27795
  return handle;
27688
27796
  } catch (err) {
@@ -27769,6 +27877,8 @@ var Launcher = class {
27769
27877
  throw new SpawnError("ESRCH");
27770
27878
  }
27771
27879
  const pid = req.pid ?? this.nextPid++;
27880
+ const cwdSlot = this.cwds.share(req.cwdSlot);
27881
+ this.cwdSlots.set(pid, cwdSlot);
27772
27882
  const scrubs = this.treeScrubs(req);
27773
27883
  const stack = this.stackFor(req.tag);
27774
27884
  const terminal = req.fds.stdioTerminal();
@@ -27803,6 +27913,7 @@ var Launcher = class {
27803
27913
  ...this.dac ? { dac: this.dac } : {},
27804
27914
  program: req.program,
27805
27915
  changes: this.changes.sab,
27916
+ ...this.heldFor(pid, cwdSlot),
27806
27917
  argv0: req.argv0,
27807
27918
  args: req.args,
27808
27919
  env: req.env,
@@ -27877,6 +27988,8 @@ var Launcher = class {
27877
27988
  (state) => state === "stopped" ? this.stopped.add(pid) : this.stopped.delete(pid)
27878
27989
  );
27879
27990
  void handle.exited.then(() => {
27991
+ this.cwds.release(this.cwdSlots.get(pid));
27992
+ this.cwdSlots.delete(pid);
27880
27993
  this.freshModules.forget(pid);
27881
27994
  const spent = handle.times?.();
27882
27995
  if (spent) this.spent.set(pid, spent);
@@ -28108,6 +28221,7 @@ var Launcher = class {
28108
28221
  const handle = await this.launch(planned, {
28109
28222
  ...deciding ? { decided: deciding.promise } : {},
28110
28223
  cwd: req.cwd,
28224
+ cwdFrom: inheritedCwd(req.chdir, this.cwdSlots.get(ppid)),
28111
28225
  ...redeemed,
28112
28226
  fds,
28113
28227
  report,
@@ -28169,6 +28283,24 @@ var Launcher = class {
28169
28283
  for (const resolve2 of waiting) resolve2(true);
28170
28284
  }, 0);
28171
28285
  }
28286
+ heldFor(pid, slot2) {
28287
+ return {
28288
+ cwds: this.cwds.sab,
28289
+ cwdSlot: slot2,
28290
+ cwdState: this.cwds.stateOf(slot2),
28291
+ cwdCheck: async () => {
28292
+ const current = this.cwdSlots.get(pid);
28293
+ return { ...await this.cwds.check(current), slot: current };
28294
+ },
28295
+ cwdChdir: (path) => this.chdirHeld(pid, path)
28296
+ };
28297
+ }
28298
+ async chdirHeld(pid, path) {
28299
+ this.cwds.release(this.cwdSlots.get(pid));
28300
+ const slot2 = await this.cwds.hold(path);
28301
+ this.cwdSlots.set(pid, slot2);
28302
+ return { slot: slot2, ...this.cwds.stateOf(slot2) };
28303
+ }
28172
28304
  forker(ppid, parent) {
28173
28305
  const { exec: _exec, pgid: _pgid, group: _group, pid: _pid, scrub: _scrub, ...image } = parent;
28174
28306
  return async (state, given) => {
@@ -28176,6 +28308,7 @@ var Launcher = class {
28176
28308
  ...image,
28177
28309
  quietOom,
28178
28310
  cwd: state.cwd ?? parent.cwd,
28311
+ cwdSlot: this.cwdSlots.get(ppid),
28179
28312
  fds,
28180
28313
  ppid,
28181
28314
  fork: state,