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.
- package/lib/wsl/confinement.js +20 -7
- package/lib/wsl/dsh-wsl-confine.sh +40 -13
- package/package.json +1 -1
package/lib/wsl/confinement.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
'
|
|
772
|
-
'
|
|
773
|
-
// Not storage: the mount flag decides, and it must be `ro` as
|
|
774
|
-
// (a mount's VFS options are a comma list; `ro`
|
|
775
|
-
|
|
776
|
-
'
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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"
|