diff --git a/.xum/skills/verify-xum/SKILL.md b/.xum/skills/verify-xum/SKILL.md index 455ead811fd..ffaa533adc4 100644 --- a/.xum/skills/verify-xum/SKILL.md +++ b/.xum/skills/verify-xum/SKILL.md @@ -41,8 +41,12 @@ The script runs on Linux only: it needs `setsid`, `flock`, `ss` and `/proc`. Eve refuses a checkout whose git dir (or, for a worktree, its common git dir) is not yours or is writable by group or others, and one with a folder above that git dir that is not yours or root's, or that group or others can write without the sticky bit: the launch lock and the -newest-run link live there. Use a checkout of your own, not a shared repository. Bun on PATH must be the version in `package.json` -`packageManager`. Launch refuses another Bun. +newest-run link live there. Use a checkout of your own, not a shared repository. Under umask 002 +(the default on Debian and Ubuntu, which give each user a private group), git makes a clone's +folder, `.git` and `.git/worktrees/` 0775, so every command refuses. Run +`chmod g-w,o-w` on the folder the message names (it names one at a time), or use a checkout made +under umask 022. Bun on PATH must be the version in `package.json` `packageManager`. Launch +refuses another Bun. ```bash .xum/skills/verify-xum/scripts/verify-xum.sh launch diff --git a/.xum/skills/verify-xum/scripts/verify-xum.sh b/.xum/skills/verify-xum/scripts/verify-xum.sh index 54bca5e62c3..9364fe55fff 100755 --- a/.xum/skills/verify-xum/scripts/verify-xum.sh +++ b/.xum/skills/verify-xum/scripts/verify-xum.sh @@ -85,28 +85,37 @@ group_is_ours() { return 1 } +# ": " with @DIR@ replaced by the folder, quoted so the command can be copied as printed (a +# folder can hold spaces, glob characters, & or \). Nothing without a fix. The fix is split at +# @DIR@ instead of ${1//@DIR@/...}: bash 5.2's patsub_replacement reads & and \ in that +# replacement again and undoes the quoting. +refusal_fix() { # refusal_fix + [[ -n $1 ]] || return 0 + printf ': %s%q%s' "${1%%@DIR@*}" "$2" "${1#*@DIR@}" +} + # A run's state.env is sourced, so the run folder and the home above it must be real folders (no # symlinks), owned by this user and writable by nobody else. -require_private_dir() { - local dir=$1 mode +require_private_dir() { # require_private_dir [] + local dir=$1 fix=${2:-} mode [[ -d $dir && ! -L $dir ]] || die "$dir is not a real folder" [[ $(stat -c %u "$dir") == "$(id -u)" ]] || die "$dir is not owned by user $(id -u)" mode=$(stat -c %a "$dir") - (((8#$mode & 8#022) == 0)) || die "$dir is writable by group or others (mode $mode)" + (((8#$mode & 8#022) == 0)) || die "$dir is writable by group or others (mode $mode)$(refusal_fix "$fix" "$dir")" } # Every folder above a run home (or a git dir) must be owned by root or this user and writable by # nobody else, unless it is sticky like /tmp (as ssh's StrictModes checks): another account could # otherwise rename the checked folder away and put its own in its place. Takes a physical path. -require_safe_ancestors() { # require_safe_ancestors [] - local dir=$1 what=${2:-the run home} owner mode +require_safe_ancestors() { # require_safe_ancestors [] [] + local dir=$1 what=${2:-the run home} fix=${3:-} owner mode while [[ $dir != / ]]; do dir=$(dirname "$dir") owner=$(stat -c %u "$dir") mode=$(stat -c %a "$dir") [[ $owner == 0 || $owner == "$(id -u)" ]] || die "$dir, above $what, is owned by user $owner" (((8#$mode & 8#022) == 0 || (8#$mode & 8#1000) != 0)) \ - || die "$dir, above $what, is writable by group or others and not sticky (mode $mode)" + || die "$dir, above $what, is writable by group or others and not sticky (mode $mode)$(refusal_fix "$fix" "$dir")" done } @@ -419,8 +428,13 @@ launch_failed() { cmd_cleanup "$VERIFY_XUM_RUN" >&2 || true } +# The process group id that the new group's leader appended to state.env, if it has. +recorded_pgid() { + sed -n 's/^export VERIFY_XUM_PGID=\([0-9][0-9]*\)$/\1/p' "$VERIFY_XUM_RUN/state.env" | tail -n 1 +} + cmd_launch() { - local t0 backend_port vite_port log existing token + local t0 backend_port vite_port log existing token leader need git curl node bun make setsid flock ss realpath agent-browser check_bun LAUNCH_DEADLINE=$((SECONDS + READY_TIMEOUT)) @@ -480,9 +494,18 @@ cmd_launch() { "$VERIFY_XUM_RUN/state.env" "${env_without_live_xum[@]}" \ make -C "$CHECKOUT" dev-server-sandbox DEV_SERVER_SANDBOX_ARGS="--clean-providers --clean-projects" \ >"$log" 2>&1 /dev/null && ! grep -qszx "VERIFY_XUM_SERVER_OF=$VERIFY_XUM_RUN_ID" /proc/[0-9]*/environ; then + VERIFY_XUM_PGID=$(recorded_pgid) + [[ -n $VERIFY_XUM_PGID ]] && break + die "the server's process group exited before it wrote its id to $VERIFY_XUM_RUN/state.env (a read-only file or a full disk?)" + fi sleep 0.1 done release_launch_guard @@ -743,9 +766,11 @@ cmd_cleanup() { local i # Read the token before the group stops: the backend then deletes server.lock. REDACT_TOKEN=$(run_token) - # agent-browser `close` on a session that does not exist starts that session first. - if [[ $(agent-browser session list 2>/dev/null) == *"$AGENT_BROWSER_SESSION"* ]]; then - agent-browser --session "$AGENT_BROWSER_SESSION" close >/dev/null 2>&1 || true + # agent-browser `close` on a session that does not exist starts that session first. From + # launch_failed this runs with the launch guard on fd 9: an agent-browser daemon started here + # must not inherit it, or it holds the flock and blocks every later launch. + if [[ $(agent-browser session list 2>/dev/null 9>&-) == *"$AGENT_BROWSER_SESSION"* ]]; then + agent-browser --session "$AGENT_BROWSER_SESSION" close >/dev/null 2>&1 9>&- || true fi if group_is_ours "$VERIFY_XUM_PGID" "$VERIFY_XUM_RUN_ID"; then kill -TERM -- "-$VERIFY_XUM_PGID" 2>/dev/null || true @@ -779,12 +804,15 @@ cmd_cleanup() { # of this user or that group or others can write: the rule for the run home and the folders # above it. A writable folder above would let another account rename the whole git dir away and # put its own in its place. The checks on the guard's open fd stay as a second line. +# Under umask 002 (the default for Debian's and Ubuntu's per-user groups) git makes a clone's +# folder, .git and .git/worktrees/ 0775, so the refusal says how to fix the folder it names. +GIT_DIR_FIX="run chmod g-w,o-w @DIR@ (git makes its folders group-writable under umask 002), or use a checkout made under umask 022" require_private_git_dirs() { local dir for dir in "$CHECKOUT_GIT_DIR" "$(git -C "$CHECKOUT" rev-parse --path-format=absolute --git-common-dir)"; do dir=$(cd -P -- "$dir" && pwd -P) - require_private_dir "$dir" - require_safe_ancestors "$dir" "the git dir $dir" + require_private_dir "$dir" "$GIT_DIR_FIX" + require_safe_ancestors "$dir" "the git dir $dir" "$GIT_DIR_FIX" done } require_private_git_dirs