dsh-wsl-desktop 0.7.5 → 0.7.6

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.
@@ -765,15 +765,28 @@ function postConditions(keep) {
765
765
  // where the flag is the authority, i.e. on a target that is not storage. For
766
766
  // a socket/FIFO/device, `[ -w ]` says nothing about the mount (see
767
767
  // {@link KERNEL_SURFACES} and the module note), so the flag is what decides.
768
- ' if [ -d "$target" ] || [ -f "$target" ]; then',
768
+ //
769
+ // WHICH test applies is decided by the inode TYPE from `stat -c %F`, NOT by
770
+ // `-f`/`-d`. Measured: after the fence binds the workspace onto itself, a
771
+ // unix socket bind-mounted UNDER the workspace answers `-f` TRUE, while the
772
+ // same socket answers `-f` FALSE before that bind — the shell test follows
773
+ // the mount the path resolves to, and the workspace bind changes what that
774
+ // is. Gating on `-f`/`-d` therefore sent the socket down the storage branch,
775
+ // where it failed with "is still writable": the issue #2 symptom this change
776
+ // exists to remove, reproduced by the fix itself. `stat` reports the inode
777
+ // class directly and is not fooled by the bind.
778
+ ' kind=$(stat -c %F "$target" 2>/dev/null) || kind="";',
779
+ ' case "$kind" in',
780
+ " 'regular file'|'regular empty file'|directory|'symbolic link')",
769
781
  // Storage inode: `[ -w ]` decides. It already covers a covered record (the
770
782
  // write would land on the reachable mount) and needs no flag test.
771
- ' if [ -w "$target" ]; then fail "$target is still writable"; fi;',
772
- ' else',
773
- // Not storage: the mount flag decides, and it must be `ro` as a comma member
774
- // (a mount's VFS options are a comma list; `ro` is not pinned to a position).
775
- ' case ",$opts," in *,ro,*) ;; *) fail "$target is not read-only (mount options: $opts)" ;; esac;',
776
- ' fi;',
783
+ ' if [ -w "$target" ]; then fail "$target is still writable"; fi;;',
784
+ ' *)',
785
+ // Not storage, or unreadable: the mount flag decides, and it must be `ro` as
786
+ // a comma member (a mount's VFS options are a comma list; `ro` has no fixed
787
+ // position).
788
+ ' case ",$opts," in *,ro,*) ;; *) fail "$target is not read-only (mount options: $opts)" ;; esac;;',
789
+ ' esac;',
777
790
  // The trailing `true` keeps the pipeline itself green: this while is the
778
791
  // LAST element of a pipeline, and anything that ends the body with a
779
792
  // non-zero status would fail the whole pipeline under `set -o pipefail` and
@@ -36,8 +36,8 @@
36
36
  # fence to the forged uid: root WRITE on the distribution's filesystem and on
37
37
  # /mnt/c, with only the namespace and NO_NEW_PRIVS left standing.
38
38
  #
39
- # This script runs as root and calls twelve tools by bare name (getent, cut, sed,
40
- # tr, mount, findmnt, grep, mountpoint, setpriv, env, bash, unshare). sudo's
39
+ # This script runs as root and calls thirteen tools by bare name (getent, cut, sed,
40
+ # tr, mount, findmnt, grep, mountpoint, setpriv, env, bash, unshare, stat). sudo's
41
41
  # env_reset does not save it: an exported PATH is replaced when sudoers sets
42
42
  # secure_path, but a PATH handed over as a sudo command-line assignment still
43
43
  # reaches us (measured on debian, debian-dev and arch), and the NOPASSWD grant is
@@ -47,7 +47,7 @@
47
47
  # here, as the FIRST statement, rather than depending on a deployment option we
48
48
  # cannot verify.
49
49
  #
50
- # The pin is unconditional and has NO fallback: the twelve tools must be reachable
50
+ # The pin is unconditional and has NO fallback: the thirteen tools must be reachable
51
51
  # in these six directories. A distribution where one is not fails CLOSED - nothing
52
52
  # runs: a missing getent leaves the caller record empty and the gate refuses it, a
53
53
  # missing cut fails the pipeline under pipefail (127) before the fence, and the
@@ -170,6 +170,11 @@ fi
170
170
  command -v unshare >/dev/null 2>&1 || { echo 'dsh-wsl-sandbox: setup failed: unshare not found' >&2; exit 97; }
171
171
  command -v setpriv >/dev/null 2>&1 || { echo 'dsh-wsl-sandbox: setup failed: setpriv not found' >&2; exit 97; }
172
172
  command -v findmnt >/dev/null 2>&1 || { echo 'dsh-wsl-sandbox: setup failed: findmnt not found' >&2; exit 97; }
173
+ # `stat` classifies the post-condition's target and is called INSIDE the fence,
174
+ # where a bare-name lookup failure would leave kind empty — that routes to the
175
+ # flag branch, which is fail-closed, but the report would then be a confusing
176
+ # "not read-only" rather than the real cause. Preflight it like the others.
177
+ command -v stat >/dev/null 2>&1 || { echo 'dsh-wsl-sandbox: setup failed: stat not found' >&2; exit 97; }
173
178
 
174
179
  # The allow-list pattern is built HERE, from the validated parameters, with every
175
180
  # entry escaped so a PATH stays data and never becomes regex syntax. Unescaped,
@@ -234,30 +239,52 @@ findmnt -rno TARGET | while IFS= read -r raw; do printf "%b\n" "$raw"; done | {
234
239
  done
235
240
  mountpoint -q /tmp || fail "/tmp is not a private tmpfs"
236
241
  findmnt -rno OPTIONS / | grep -q "^ro" || fail "/ is not read-only"
242
+ # NOTE: this block lives inside the single-quoted FENCE string. A BACKTICK here
243
+ # would close the quoting and let bash run its contents as a command
244
+ # substitution — measured: eight backticks around the test -w mention made the
245
+ # helper exit 127 with "read-only: command not found" before the fence ran at
246
+ # all. Keep this comment free of backticks and of single quotes.
237
247
  # The postcondition pairs two tests, and each is load-bearing where the other is
238
248
  # blind. A target can be STACKED — findmnt lists one record per mount — and
239
- # `remount,ro,bind` by path only ever reaches the TOPMOST one (measured: after
249
+ # remount,ro,bind by path only ever reaches the TOPMOST one (measured: after
240
250
  # remounting /run/user, a covered record still reported rw while a write through
241
251
  # the path failed EROFS — unreachable storage, not a hole). So:
242
- # * a REGULAR FILE or DIRECTORY is judged by `[ -w ]`: it answers about the
252
+ # * a REGULAR FILE or DIRECTORY is judged by test -w: it answers about the
243
253
  # mount a write would actually land on, covered record or not;
244
254
  # * anything else (socket, FIFO, device) is judged by the MOUNT FLAG, because
245
- # `[ -w ]` is meaningless there: access(W_OK) narrows the kernel's read-only
255
+ # test -w is meaningless there: access(W_OK) narrows the kernel read-only
246
256
  # answer to S_ISREG/S_ISDIR/S_ISLNK (sb_permission), do_faccessat() adds a
247
257
  # special_file() exclusion, and this fragment runs as root (CAP_DAC_OVERRIDE)
248
258
  # before the setpriv drop. A socket therefore reported "writable" on a mount
249
259
  # that WAS remounted read-only: measured as exit 97 for the unix socket under
250
260
  # /mnt/wsl/podman-sockets with podman machine running, refusing every
251
- # confined command including a bare `true` (github issue #2).
261
+ # confined command including a bare true (github issue #2).
252
262
  findmnt -rno TARGET,OPTIONS | while IFS=" " read -r raw opts; do
253
263
  target=$(printf "%b" "$raw")
254
264
  if printf "%s\n" "$target" | grep -Eq "^(${DROP_EXEMPT})\$"; then continue; fi
255
- if [[ -d "$target" || -f "$target" ]]; then
256
- if [[ -w "$target" ]]; then fail "$target is still writable"; fi
257
- else
258
- # Comma-delimited membership, not a ^ro prefix: ro is not pinned to a position.
259
- case ",$opts," in *,ro,*) ;; *) fail "$target is not read-only (mount options: $opts)" ;; esac
260
- fi
265
+ # WHICH test applies is decided by the inode TYPE, read from stat, and NOT by
266
+ # the -f/-d shell tests. Measured: after the fence binds the workspace onto
267
+ # itself, a unix socket bind-mounted UNDER the workspace answers -f TRUE, while
268
+ # the same socket answers -f FALSE before that bind — the shell test follows the
269
+ # mount the path resolves to and the workspace bind changes what that is. So
270
+ # -f/-d cannot tell "storage" from "special file" here, and using them sent the
271
+ # socket down the -w branch, where it failed with "is still writable" exactly as
272
+ # issue #2 reported. stat -c %F reports the inode class directly.
273
+ kind=$(stat -c %F "$target" 2>/dev/null) || kind=""
274
+ # NOTE: double quotes, never single: this block is inside the single-quoted
275
+ # FENCE string, so a single quote would close it. The case PATTERNS below are
276
+ # matched literally, so their quoting form is part of the comparison.
277
+ case "$kind" in
278
+ "regular file"|"regular empty file"|directory|"symbolic link")
279
+ # Storage: test -w answers about the mount a write would actually land on.
280
+ if [[ -w "$target" ]]; then fail "$target is still writable"; fi
281
+ ;;
282
+ *)
283
+ # Not storage (socket, fifo, device, or unreadable): the mount FLAG decides.
284
+ # Comma-delimited membership, not a ^ro prefix: ro is not pinned to a position.
285
+ case ",$opts," in *,ro,*) ;; *) fail "$target is not read-only (mount options: $opts)" ;; esac
286
+ ;;
287
+ esac
261
288
  true
262
289
  done
263
290
  cd "$DROP_CWD"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-wsl-desktop",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "icon": "./lib/icon.svg",