#!/usr/bin/env bash # Row 195 (#1693): the Case 1 acceptance on 10.1.1.53, design r7 section 5 # (agents/darkwing/work/queue-195/DESIGN.md). Deviations and decisions: agents/darkwing/work/queue-195/BUILD.md. # # case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N # # Root runs it on the Case 1 host after the install window, from a root-owned checkout. It reads the # user, home, copy, node, claude, main port and tester socket marker from the args file, which must pass stack-upgrade.sh's # rules and be a factory file for --business. Every command that runs the stack's code runs as # `sudo -u USER -H env PATH=:/usr/local/bin:/usr/bin:/bin` from /. Root itself reads ss, # /proc, the account database and file metadata, and runs these: systemctl show and is-enabled (F1, # P11), curl on the Console's loopback healthz (F2), sudo -l (P1, P3), sudo -u jarvis bwrap --version # and hostcheck.sh --check (P9), and node to parse managed-settings.json (F9). F9 runs node only when # --node is the pinned path and it and every directory above it are root's with no group or other # write. The script changes nothing on the host but the message F3 sends and its reply, and it never # opens claude-session.env. A bounded config/target query imports the copy only as the stack # user, with sudo and an empty environment. Root parses its two facts in bash. P10 uses the # fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation. # The config/probe supervisor is fixed root-validated system Bash plus setsid # and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root # independently bounds its sudo wait. All legacy calls use the same payload: # talk320/330 s (conversation --wait300), precheck200/210 s (its Claude probe180), # Claude/Node versions5/7 s, PM launches10/12 s. Stderr retains its last65536bytes, # stdout streams to the original caller. Supervisor0 alone permits success. # A member that changes uid and cannot be killed refuses inner-cleanup; hostile # same-uid setsid-away or a killed/stalled supervisor remains the R3 residual. # # One line per check, ` `, then `count N` for each mark and a # result line. F checks are `pass` or `FAIL`. P checks are `closed`, `accepted ` or # `unnamed`, which means no Q182, Q183 or Mos ruling names the path; each unnamed line goes to Mos. # # Exit 0 when every F check passes, 1 when one fails, 2 on usage or a refusal (a bad args file, or # --root misused). A P check never changes the exit. # # --org ID is the same option as --business ID, in three scopes: this script's own arguments, the args # file's unit arguments before --, and its serve arguments after it. In each scope a spelling may appear # once, and the other spelling must carry the same value; the args file needs a selector in both of its # scopes, and all three must name the same business. The talk and precheck calls still pass --business, # which every copy takes. # # Test seam, for packages/cli/tests/case1-accept.test.mjs only. Root takes none: # --root DIR prefixes the args file, /etc, /proc, the stack's home, the host check and node, and # reads the account database from DIR/etc/passwd and DIR/etc/group instead of getent set -uo pipefail usage() { echo "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N" >&2; exit 2; } refuse() { echo "case1-accept: refusing: $*" >&2; exit 2; } # selector.sh defines functions only, from this script's own root-owned checkout; parsing runs no program. case "${BASH_SOURCE[0]}" in */*) SELECTOR_SH="${BASH_SOURCE[0]%/*}/selector.sh" ;; *) SELECTOR_SH=./selector.sh ;; esac . "$SELECTOR_SH" || refuse "cannot read $SELECTOR_SH" business_id_ok() { [[ "$1" =~ ^[a-z0-9][a-z0-9-]*$ ]]; } invocation_refused() { case "$1" in missing|flag|invalid) usage ;; *) refuse "$2" ;; esac; } BUSINESS="" ARGS_FILE="" CLAUDE_PIN="" NODE_PIN="" ROOT="" while [ $# -gt 0 ]; do case "$1" in --business|--org) selector_take invocation id invocation_refused business_id_ok "$1" "${@:2:1}"; BUSINESS="$2"; shift 2 ;; --args) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] || usage; ARGS_FILE="$2"; shift 2 ;; --claude-pin) [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; CLAUDE_PIN="$2"; shift 2 ;; --node-pin) [ $# -ge 2 ] && [[ "$2" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage; NODE_PIN="$2"; shift 2 ;; --root) [ $# -ge 2 ] && [[ "$2" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [ -d "$2" ] || usage; ROOT="${2%/}"; shift 2 ;; *) usage ;; esac done [ -n "$BUSINESS" ] && [ -n "$ARGS_FILE" ] && [ -n "$CLAUDE_PIN" ] && [ -n "$NODE_PIN" ] || usage if [ "$(id -u)" = 0 ]; then [ -z "$ROOT" ] || refuse "--root is a test seam, and root takes none" PATH=/usr/sbin:/usr/bin:/sbin:/bin else [ -n "$ROOT" ] || refuse "run it as root" fi cd / || refuse "cannot cd to /" # --- The args file, by stack-upgrade.sh's read_args rules, plus the serve ports --- f="$ROOT$ARGS_FILE" [ -f "$f" ] && [ ! -L "$f" ] || refuse "no regular file $ARGS_FILE" st="$(stat -c '%u %a' -- "$f")" || refuse "cannot stat $ARGS_FILE" [ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || refuse "$ARGS_FILE must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })" mapfile -t ARGS < "$f" || refuse "cannot read $ARGS_FILE" unit_refused() { refuse "$ARGS_FILE before --: $2"; } serve_refused() { refuse "$ARGS_FILE after --: $2"; } S_USER="" S_HOME="" A_BUSINESS="" NODE="" COPY_ID="" CLAUDE="" PROFILE="" PORT="" TESTER_SOCKET=0 copies=0 past="" served="" for ((i = 0; i < ${#ARGS[@]}; i++)); do a="${ARGS[i]}" v="${ARGS[i + 1]:-}" if [ -n "$past" ]; then case "$a" in --port) PORT="$v"; i=$((i + 1)) ;; --business|--org) selector_take serve id serve_refused business_id_ok "$a" "$v" [ "$v" = "$BUSINESS" ] || refuse "serve business differs"; served=1; i=$((i + 1)) ;; --tester-socket) TESTER_SOCKET=$((TESTER_SOCKET + 1)) ;; --tester-port) refuse "case1-accept.sh retires tester TCP; use --tester-socket" ;; *) refuse "unknown serve flag $a" ;; esac continue fi case "$a" in --) past=1 ;; --pm) ;; --user) S_USER="$v"; i=$((i + 1)) ;; --home) S_HOME="$v"; i=$((i + 1)) ;; --business|--org) selector_take unit id unit_refused business_id_ok "$a" "$v"; A_BUSINESS="$v"; i=$((i + 1)) ;; --node) NODE="$v"; i=$((i + 1)) ;; --claude) CLAUDE="$v"; i=$((i + 1)) ;; --profile) PROFILE="$v"; i=$((i + 1)) ;; --copy) COPY_ID="$v"; copies=$((copies + 1)); i=$((i + 1)) ;; *) refuse "$ARGS_FILE has $a before --, which case2-unit.sh does not take" ;; esac done [ "$copies" = 1 ] || refuse "$ARGS_FILE needs exactly one --copy before --, not $copies" [ -n "$served" ] || refuse "$ARGS_FILE needs --business or --org after --" [[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$A_BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] && [[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] && [[ "$CLAUDE" =~ ^/[A-Za-z0-9/._+@-]+$ ]] || refuse "$ARGS_FILE lacks a plain --user, --home, --business, --node, --claude or --copy" [ "$PROFILE" = factory ] || refuse "$ARGS_FILE has --profile '${PROFILE}', not factory" [ "$A_BUSINESS" = "$BUSINESS" ] || refuse "$ARGS_FILE is for business $A_BUSINESS, not $BUSINESS" [[ "$PORT" =~ ^[1-9][0-9]{0,3}$ ]] && [ "$PORT" -le 1023 ] || refuse "$ARGS_FILE lacks a privileged numeric --port after --" [ "$TESTER_SOCKET" = 1 ] || refuse "$ARGS_FILE needs one bare --tester-socket after --" COPY="$S_HOME/.local/share/mosaic-stack/copies/$COPY_ID" DATA_ROOT="$S_HOME/.mosaic-dev" CRED="$S_HOME/.config/mosaic-dev/secrets/claude-session.env" BUSINESS_FILE="$S_HOME/.config/mosaic-dev/businesses/$BUSINESS.json" BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh SETTINGS=/etc/claude-code/managed-settings.json LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)' WORK="$(mktemp -d)" || refuse "cannot make a temp dir" chmod 700 "$WORK" || refuse "cannot protect temp dir" trap 'rm -rf -- "$WORK"' EXIT # Row253: bounded unprivileged metadata, parsed by root without running copy code. # row253-supervisor-begin IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || : set +m seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3 child="" timer="" session_ref="" verified=0 stat_fields() { local pid="$1" raw="" rest file="/proc/$1/stat" if [ "$pid" = "${child:-}" ] && [ -n "${session_ref:-}" ]; then file="/proc/self/fd/$session_ref/stat" fi # EOF is expected: stat has no NUL. Decide on content, not read's status. IFS= read -r -d '' raw 2>/dev/null < "$file" || : [ -n "$raw" ] || { [ -e "$file" ] && return 1; return 3; } rest="${raw##*) }" [ "$rest" != "$raw" ] || return 1 read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest" [[ "$ST_STATE" =~ ^[A-Za-z]$ && "$ST_SESSION" =~ ^[0-9]+$ && "$ST_GROUP" =~ ^[0-9]+$ ]] } clean_session() { # Fixed, isolated stdlib code, never imported from the copy or its cwd. # Buffered reads avoid bash read -d's one-byte reads over the host inventory. /usr/bin/python3 -I -S -c ' import os, signal, sys, time session = int(sys.argv[1]) until = None anchor_fd = None def fields(pid): try: with open("/proc/" + pid + "/stat", "rb") as f: raw = f.read() except (FileNotFoundError, ProcessLookupError): return None # A disappearing task can return EOF while its proc entry is still visible. # Retry once; a persistent unreadable/malformed entry refuses cleanup. if not raw: try: with open("/proc/" + pid + "/stat", "rb") as f: raw = f.read() except (FileNotFoundError, ProcessLookupError): return None rest = raw.rsplit(b") ", 1) if len(rest) != 2: raise ValueError("stat") words = rest[1].split() if len(words) < 4 or len(words[0]) != 1 or not words[0].isalpha(): raise ValueError("stat") return words[0], int(words[3]) def original_leader(): # The inherited proc directory refers to the original task, even if its # numeric PID is reused. A live trusted leader keeps this SID allocated. info = fields("self/fd/" + sys.argv[2]) if info is None or info[1] != session or info[0] in (b"Z", b"X"): raise ValueError("lost session anchor") return info try: anchor_fd = os.pidfd_open(session) original_leader() while True: original_leader() live = False for pid in os.listdir("/proc"): if not pid.isdecimal() or int(pid) == session: continue info = fields(pid) if info is None or info[1] != session or info[0] in (b"Z", b"X"): continue live = True try: # Pin the task before rechecking membership: a recycled numeric # pid must never redirect our signal to an unrelated process. fd = os.pidfd_open(int(pid)) try: current = fields(pid) if current is not None and current[1] == session and current[0] not in (b"Z", b"X"): # Check AFTER membership: while the original anchor is # alive, this SID cannot name a later session. The pidfd # then prevents task reuse between this check and kill. original_leader() signal.pidfd_send_signal(fd, signal.SIGKILL) finally: os.close(fd) except ProcessLookupError: pass except OSError: info = fields(pid) if info is not None and info[0] not in (b"Z", b"X"): raise if not live and original_leader()[0] == b"T": # The trusted leader is stopped after waiting for the command. # With all other members dead it may return the saved command code. signal.pidfd_send_signal(anchor_fd, signal.SIGCONT) break now = time.monotonic() # Inventory pass 1 remains outside the 250 ms confirmation budget. if until is None: until = now + 0.25 if now >= until: raise TimeoutError("cleanup") time.sleep(0.01) except Exception: sys.exit(1) finally: if anchor_fd is not None: os.close(anchor_fd) ' "$child" "$session_ref" } finish() { local rc="$?" trap - EXIT HUP INT TERM if [ "$verified" = 1 ]; then clean_session || rc=86 elif [ -n "$child" ]; then kill -KILL "$child" 2>/dev/null || : fi # A refused cleanup may leave an unkillable member. Do not hide rc 86 # behind an unbounded wait; the caller retains its existing outer guard. if [ "$rc" != 86 ] && [ -n "$child" ]; then local command_rc=0 wait "$child" 2>/dev/null || command_rc=$? [ "$rc" != 0 ] || [ "$command_rc" = 0 ] || rc=87 fi [ -z "$session_ref" ] || exec {session_ref}<&- if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi exit "$rc" } trap finish EXIT trap 'exit 88' HUP INT TERM "$sleep_bin" "$seconds" & timer=$! # The trusted session leader stays alive through cleanup. Copy code runs as # its child; only after cleanup continues the stopped leader does it return # the saved command status. An open proc directory pins its task identity. "$setsid_bin" /bin/bash -c ' set +m kill -STOP "$BASHPID" "$@" & command=$! rc=0; wait "$command" 2>/dev/null || rc=$? kill -STOP "$BASHPID" exit "${rc}" ' -- "$@" & child=$! { exec {session_ref}<"/proc/$child"; } 2>/dev/null || exit 85 # Allow session creation the whole inner budget, rather than a 200 ms margin. while :; do rc=0; stat_fields "$child" || rc=$? [ "$rc" != 3 ] || exit 85 if [ "$rc" = 0 ] && [ "$ST_SESSION" = "$child" ] && [ "$ST_STATE" = T ] && [ "$ST_PARENT" = "$$" ]; then verified=1 kill -CONT "$child" 2>/dev/null || exit 85 break fi if ! kill -0 "$timer" 2>/dev/null; then # Persistent unreadable metadata remains a session refusal, not a # command timeout. A transient read gets only this same inner budget. [ "$rc" = 0 ] || exit 85 exit 81 fi "$sleep_bin" 0.002 done [ "$verified" = 1 ] || exit 85 while :; do rc=0; stat_fields "$child" || rc=$? [ "$rc" = 0 ] || exit 86 case "$ST_STATE" in Z|X) exit 86 ;; esac [ "$ST_STATE" != T ] || break kill -0 "$timer" 2>/dev/null || exit 81 "$sleep_bin" 0.01 done kill -0 "$timer" 2>/dev/null || exit 81 exit 0 MOSAIC_TESTER_SUPERVISOR # row253-supervisor-end tester_tools() { local input target p st magic TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD="" TS_TAIL="" for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head /usr/bin/python3 "$@"; do target="$(realpath -e -- "$input" 2>/dev/null)" || return 1 case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac [ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1 p="$target" while :; do st="$(stat -c '%u %a' -- "$p" 2>/dev/null)" || return 1 [ "${st% *}" = 0 ] && [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1 [ "$p" != / ] || break p="${p%/*}"; [ -n "$p" ] || p=/ done magic="$(/usr/bin/od -An -tx1 -N4 -- "$target" 2>/dev/null | tr -d ' \n')" [ "$magic" = 7f454c46 ] || return 1 case "$input" in /bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;; /usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;; /usr/bin/head) TS_HEAD="$target" ;; /usr/bin/tail) TS_TAIL="$target" ;; esac done } tester_capture_reason() { case "$1" in 81) echo inner-timeout ;; 82) echo outer-timeout ;; 83) echo output-limit ;; 84) echo supervisor-tools ;; 85) echo inner-session ;; 86) echo inner-cleanup ;; *) echo inner-command ;; esac } tester_capture() ( local inner="$1" outer="$2" maximum="$3" user="$4" home="$5" path="$6" file notice child="" rc=0; shift 6 tester_tools || exit 84 file="$(mktemp)" || exit 88 notice="$(mktemp)" || { rm -f -- "$file"; exit 88; } trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$file" "$notice"' EXIT trap 'exit 88' HUP INT TERM LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c ' set -o pipefail maximum="$1"; head_bin="$2"; shift 2 "$@" 2>/dev/null | "$head_bin" -c "$((maximum + 1))" ' -- "$maximum" "$TS_HEAD" sudo -u "$user" -H env -i "HOME=$home" "PATH=$path" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$@" > "$file" 2> "$notice" & child=$! wait "$child" 2>/dev/null || rc=$? kill -KILL -- "-$child" 2>/dev/null || : # Only GNU timeout and the trusted head can write this root-side channel. if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then exit 82; fi [ "$(wc -c < "$file")" -le "$maximum" ] || exit 83 [ "$rc" = 0 ] || { case "$rc" in 81|85|86|87|88) exit "$rc" ;; *) exit 87 ;; esac; } cat -- "$file" ) # row276-legacy-begin # The runner preserves a failed command's display code. Its exit status, not # this dropped-uid trailer, controls supervisor success. Root reads only a tail. LEGACY_COMMAND_RUNNER=' const {spawnSync}=require("node:child_process"); const {constants}=require("node:os"); const [tag,command,...args]=process.argv.slice(1); const r=spawnSync(command,args,{stdio:"inherit"}); const code=Number.isInteger(r.status)?r.status:r.signal?128+(constants.signals[r.signal]??0):127; if(r.error)process.stderr.write("legacy command "+(r.error.code??"failed")+"\n"); process.stderr.write("\nMOSAIC_LEGACY_STATUS="+code+"\n"); process.exitCode=code; ' legacy_display_rc() { local tag="$1" fallback="$2" value="" [ ! -f "$WORK/legacy-$tag.rc" ] || value="$(<"$WORK/legacy-$tag.rc")" if [[ "$value" =~ ^[1-9][0-9]{0,2}$ ]] && [ "$value" -le 255 ]; then printf '%s' "$value" else printf '%s' "$fallback"; fi } legacy_run() ( local tag="$1" inner="$2" outer="$3" notice errors output size child="" rc=0 raw suffix display=87 valid=0; shift 3 # WORK is root's private mktemp directory. The dropped uid cannot write it. printf '%s\n' 84 > "$WORK/legacy-$tag.rc" tester_tools /usr/bin/tail || { printf 'case1-accept: %s supervisor-tools\n' "$tag" >&2; exit 84; } notice="$(mktemp "$WORK/legacy-notice.XXXXXX")" || exit 88 errors="$(mktemp "$WORK/legacy-stderr.XXXXXX")" || { rm -f -- "$notice"; exit 88; } output="$(mktemp "$WORK/legacy-stdout.XXXXXX")" || { rm -f -- "$notice" "$errors"; exit 88; } trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$notice" "$errors" "$output"' EXIT trap 'exit 88' HUP INT TERM LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c ' errors="$1"; output="$2"; shift 2 exec "$@" > "$output" 2> "$errors" ' -- "$errors" "$output" sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$NODE" -e "$LEGACY_COMMAND_RUNNER" -- "$tag" "$@" 2> "$notice" & child=$! wait "$child" 2>/dev/null || rc=$? kill -KILL -- "-$child" 2>/dev/null || : if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then rc=82; fi # Sentinel retains the terminal newline through command substitution. raw="$("$TS_TAIL" -c 65536 -- "$errors" && printf .)" || { raw=""; rc=88; } raw="${raw%.}" suffix=$'\nMOSAIC_LEGACY_STATUS=' if [[ "$raw" =~ $'\nMOSAIC_LEGACY_STATUS='(0|[1-9][0-9]{0,2})$'\n'$ ]] && [ "${BASH_REMATCH[1]}" -le 255 ]; then display="${BASH_REMATCH[1]}"; valid=1 raw="${raw%"$suffix"*}" fi # Only root publishes a fixed-size snapshot. A residual keeps regular-file # descriptors, never the caller's pipe. Output precedes the stderr tail. if [ "$rc" = 0 ] || { [ "$rc" = 87 ] && [ "$valid" = 1 ] && [ "$display" -gt 0 ]; }; then size="$(wc -c < "$output")" || rc=88 if [[ "$size" =~ ^[0-9]+$ ]]; then "$TS_HEAD" -c "$size" -- "$output" || rc=88 else rc=88; fi fi printf '%s' "$raw" >&2 if [ "$rc" = 0 ]; then printf '%s\n' 0 > "$WORK/legacy-$tag.rc" exit 0 fi if [ "$rc" = 87 ] && [ "$valid" = 1 ] && [ "$display" -gt 0 ]; then printf '%s\n' "$display" > "$WORK/legacy-$tag.rc" exit 87 fi case "$rc" in 81|82|84|85|86|88) ;; *) rc=87 ;; esac printf '%s\n' "$rc" > "$WORK/legacy-$tag.rc" if [ -n "$raw" ] && [[ "$raw" != *$'\n' ]]; then printf '\n' >&2; fi printf 'case1-accept: %s %s\n' "$tag" "$(tester_capture_reason "$rc")" >&2 exit "$rc" ) # row276-legacy-end TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));' tester_facts() { local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$' [[ "$1" =~ $pattern ]] || return 1 TS_DATA="${BASH_REMATCH[1]}" TS_MARKED="${BASH_REMATCH[2]}" } tester_location() { local LC_ALL=C data="$1" home="$2" uid="$3" prefix="$4" part at="" st TS_REASON="" TS_SOCKET="" case "$data" in /*) ;; *) TS_REASON=not-absolute; return 1 ;; esac case "$data" in *[!A-Za-z0-9/._@+-]*) TS_REASON=not-ascii; return 1 ;; esac case "$data/" in */./*|*/../*|*//* ) TS_REASON=not-normalized; return 1 ;; esac [ "${data%/}" = "$data" ] || { TS_REASON=not-normalized; return 1; } case "$data" in "$home"/*) ;; *) TS_REASON=outside-home; return 1 ;; esac case "$home/" in /home/*|/root/*|/run/user/*|/tmp/*|/var/tmp/*) TS_REASON=protected-private-home; return 1 ;; esac TS_SOCKET="$data/console/tester.sock" [ "${#TS_SOCKET}" -le 107 ] || { TS_REASON=too-long; return 1; } local -a parts IFS=/ read -r -a parts <<< "$data" for part in "${parts[@]}"; do [ -n "$part" ] || continue at="$at/$part" [ ! -L "$prefix$at" ] || { TS_REASON=symlink-component; return 1; } if [ -e "$prefix$at" ]; then [ -d "$prefix$at" ] || { TS_REASON=component-type; return 1; } fi done [ "${5:-}" != observe ] || return 0 local parent="$prefix$data/console" [ ! -L "$parent" ] || { TS_REASON=symlink-parent; return 1; } if [ -e "$parent" ]; then [ -d "$parent" ] || { TS_REASON=parent-type; return 1; } st="$(stat -c '%u %a' -- "$parent")" || { TS_REASON=parent-stat; return 1; } [ "${st% *}" = "$uid" ] || { TS_REASON=parent-owner; return 1; } [[ "${st#* }" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#077) == 0 )) || { TS_REASON=parent-mode; return 1; } fi } # tester-location-end # --- Output --- MARKS=(pass FAIL closed "accepted Q182+Q183" "accepted Q183" "accepted mos-1096" unnamed) declare -A COUNT=() F_FAILED=0 # Text the stack's user wrote, made safe to print: printable ASCII, one line, at most 200 bytes. clean() { tr -c '[:print:]' ' ' <<< "$1" | cut -c1-200 | sed 's/ *$//'; } line() { local id="$1" path="$2" mark="$3"; shift 3; echo "$id $path $mark $*"; COUNT[$mark]=$(( ${COUNT[$mark]:-0} + 1 )); } fpass() { line "$1" "$2" pass "${@:3}"; } ffail() { line "$1" "$2" FAIL "${@:3}"; F_FAILED=1; } # --- The account database: getent on the host, the seam's files under --root --- db() { if [ -n "$ROOT" ]; then if [ $# -gt 1 ]; then awk -F: -v k="$2" '$1 == k' "$ROOT/etc/$1"; else cat "$ROOT/etc/$1"; fi else getent "$@" fi } uid_of() { db passwd "$1" | cut -d: -f3; } gid_of() { db passwd "$1" | cut -d: -f4; } # The group ids USER belongs to, as `id -G USER` prints them: the primary gid, then each group # whose member list names USER. groups_of() { local g; g="$(gid_of "$1")" [ -n "$g" ] || return 0 { echo "$g"; db group | awk -F: -v u="$1" '{ n = split($4, m, ","); for (i = 1; i <= n; i++) if (m[i] == u) print $3 }'; } | awk '!seen[$0]++' } names_of_groups() { local g; for g in "$@"; do db group | awk -F: -v g="$g" '$3 == g { print $1 }'; done; } MOSAIC_UID="$(uid_of "$S_USER")" [[ "$MOSAIC_UID" =~ ^[1-9][0-9]*$ ]] || refuse "stack account has no non-root uid" [ "$(db passwd "$S_USER" | cut -d: -f6)" = "$S_HOME" ] || refuse "saved home differs from account home" QUERY="$(tester_capture 5 7 4096 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || refuse "tester config query $(tester_capture_reason "$?")" tester_facts "$QUERY" || refuse "tester config query has invalid facts" [ "$TS_MARKED" = true ] || refuse "target does not support --tester-socket" tester_location "$TS_DATA" "$S_HOME" "$MOSAIC_UID" "$ROOT" observe || refuse "tester path refuses $TS_REASON" TESTER_PATH="$TS_SOCKET" # --- systemd, /proc and listeners --- # KEY=VALUE lines for UNIT's properties. unit_show() { local u="$1"; shift; systemctl show "$u" "${@/#/-p}" 2>/dev/null; } prop() { sed -n "s/^$2=//p" <<< "$1" | tail -n 1; } # The real, effective, saved and filesystem uids of PID, space-separated. proc_uids() { awk '$1 == "Uid:" { print $2, $3, $4, $5 }' "$ROOT/proc/$1/status" 2>/dev/null; } proc_label() { local l; l="$(cat "$ROOT/proc/$1/attr/current" 2>/dev/null)" || return 1; printf '%s' "${l%$'\n'}"; } # One `ADDRESS PIDS` line per TCP listener on port $1, PIDS comma-separated. listeners() { ss -Hltnp 2>/dev/null | awk -v p="$1" '{ n = split($4, a, ":"); if (a[n] != p) next addr = substr($4, 1, length($4) - length(p) - 1); pids = ""; s = $0 while (match(s, /pid=[0-9]+/)) { pids = pids (pids == "" ? "" : ",") substr(s, RSTART + 4, RLENGTH - 4); s = substr(s, RSTART + RLENGTH) } print addr, pids }' } # 0 when port $1 has a listener and every one is on 127.0.0.1, held by pid $2 alone. loopback_by() { local addr pids p any=0 [ -n "$2" ] && [ "$2" != 0 ] || return 1 while read -r addr pids; do [ -n "$addr" ] || continue any=1 [ "$addr" = 127.0.0.1 ] || return 1 for p in ${pids//,/ }; do [ "$p" = "$2" ] || return 1; done [ -n "$pids" ] || return 1 done <<< "$(listeners "$1")" [ "$any" = 1 ] } listener_text() { local t; t="$(listeners "$1" | tr '\n' ';')"; echo "${t:-none}"; } BUS_SHOW="$(unit_show "$BUS_UNIT" ActiveState User MainPID)" CON_SHOW="$(unit_show "$CON_UNIT" ActiveState User MainPID)" CON_PID="$(prop "$CON_SHOW" MainPID)" # --- F checks --- check_f1() { local u show state user pid uids why="" for u in "$BUS_UNIT" "$CON_UNIT"; do if [ "$u" = "$BUS_UNIT" ]; then show="$BUS_SHOW"; else show="$CON_SHOW"; fi state="$(prop "$show" ActiveState)" user="$(prop "$show" User)" pid="$(prop "$show" MainPID)" uids="$(proc_uids "${pid:-0}")" [ "$state" = active ] || { why+="$u is ${state:-unknown}; "; continue; } [ "$user" = "$S_USER" ] || { why+="$u has User=${user:-unset}; "; continue; } [ -n "$MOSAIC_UID" ] && [ "$uids" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || why+="$u MainPID ${pid:-none} has uids '${uids:-unreadable}', not $S_USER's ${MOSAIC_UID:-unknown}; " done if [ -z "$why" ]; then fpass F1 units-user "$BUS_UNIT and $CON_UNIT are active as $S_USER (uid $MOSAIC_UID)" else ffail F1 units-user "${why%; }"; fi } check_f2() { local rc=0 curl -fsS --max-time 10 -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null || rc=$? if [ "$rc" != 0 ]; then ffail F2 console-loopback "curl http://127.0.0.1:$PORT/healthz exited $rc" elif ! loopback_by "$PORT" "$CON_PID"; then ffail F2 console-loopback "port $PORT listeners are '$(listener_text "$PORT")', not 127.0.0.1 by the Console's MainPID ${CON_PID:-none}" else fpass F2 console-loopback "healthz answers, and $PORT is bound to 127.0.0.1 only, by MainPID $CON_PID"; fi } # F5's scan: an outer /usr/bin/bwrap owned by the stack's user with a claude descendant. Sets F5_OK # when one pair has both labels exactly, F5_WHY to the last mismatch otherwise. F5_OK="" F5_WHY="no /usr/bin/bwrap process of $S_USER with a claude descendant was seen while the reply was pending" scan_f5() { local pid ppid uids kids q c argv0 comm lo lc local -A parent=() children=() bw=() # One read of every PPid line and one of every exe link, so a scan stays short on a busy host. while read -r pid ppid; do parent[$pid]="$ppid" children[$ppid]+=" $pid" done < <(grep -H '^PPid:' "$ROOT"/proc/[0-9]*/status 2>/dev/null | sed -n 's#^.*/proc/\([0-9]*\)/status:PPid:[[:space:]]*\([0-9]*\)$#\1 \2#p') while read -r pid; do [[ "$pid" =~ ^[0-9]+$ ]] && [ -n "${parent[$pid]:-}" ] && bw[$pid]=1 done < <(find "$ROOT/proc" -mindepth 2 -maxdepth 2 -name exe -lname /usr/bin/bwrap -printf '%h\n' 2>/dev/null | sed 's#.*/##') for pid in "${!bw[@]}"; do [ -z "${bw[${parent[$pid]}]:-}" ] || continue uids="$(proc_uids "$pid")" [ -n "$MOSAIC_UID" ] && [ "${uids%% *}" = "$MOSAIC_UID" ] || continue # Every descendant, breadth first. kids=() q=("$pid") while [ ${#q[@]} -gt 0 ]; do for c in ${children[${q[0]}]:-}; do kids+=("$c") q+=("$c"); done q=("${q[@]:1}") done for c in "${kids[@]}"; do argv0="$(tr '\0' '\n' < "$ROOT/proc/$c/cmdline" 2>/dev/null | sed -n 1p)" comm="$(cat "$ROOT/proc/$c/comm" 2>/dev/null)" [[ "${argv0##*/}" == claude* ]] || [[ "$comm" == claude* ]] || continue lo="$(proc_label "$pid")" lc="$(proc_label "$c")" if [ "$lo" = "$LABEL_OUTER" ] && [ "$lc" = "$LABEL_CHILD" ]; then F5_OK="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc'"; return 0 fi F5_WHY="bwrap $pid reads '$lo' and its claude descendant $c reads '$lc', not '$LABEL_OUTER' and '$LABEL_CHILD'" done done return 1 } # F3 and F5 together: the message goes out in the background and the scan runs until it is answered. check_f3_f5() { local marker text rc=0 shown out err n tpid marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')" text="$marker: this is the Case 1 acceptance; reply with one short line." legacy_run talk 320 330 "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" & tpid=$! while :; do scan_f5 && break kill -0 "$tpid" 2>/dev/null || break sleep 0.5 done wait "$tpid" || rc=$? # Both the sent line and the reply are on stdout; an error is on stderr. out="$(cat "$WORK/talk.out")" err="$(cat "$WORK/talk.err")" n="$(grep -c . <<< "$out")" if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then fpass F3 tester-reply "chief replied to $marker" else shown="$(legacy_display_rc talk "$rc")" ffail F3 tester-reply "talk exited $shown with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")" fi if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi } check_f4() { local rc=0 shown out out="$(legacy_run precheck 200 210 "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$? if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0" else shown="$(legacy_display_rc precheck "$rc")"; ffail F4 precheck "case2-precheck.sh --profile factory exited $shown; last: $(clean "$(tail -n 1 <<< "$out")")"; fi } check_f6() { local want="/opt/mosaic-stack/claude-$CLAUDE_PIN/" v rc=0 [[ "$CLAUDE" == "$want"* ]] || { ffail F6 claude-pin "--claude $CLAUDE is not under $want"; return; } [[ "$CLAUDE/" != */../* ]] || { ffail F6 claude-pin "--claude $CLAUDE has a .. segment"; return; } v="$(legacy_run claude-version 5 7 "$CLAUDE" --version 2> "$WORK/claude-version.err")" || rc=$? v="$(sed -n 1p <<< "$v")" if [ "$rc" = 0 ] && [ "$v" = "$CLAUDE_PIN (Claude Code)" ]; then fpass F6 claude-pin "$CLAUDE prints '$v'" elif [ "$rc" != 0 ]; then ffail F6 claude-pin "claude-version $(tester_capture_reason "$rc"); last: $(clean "$(tail -c 200 "$WORK/claude-version.err")")" else ffail F6 claude-pin "$CLAUDE --version prints '$(clean "$v")', not '$CLAUDE_PIN (Claude Code)'"; fi } # The PM check confirm runs (stack-release.sh pm_external): exactly one live launches list entry for # launch.by, for this business. The stack's user runs the whole check and prints one result line. F7_JS=' const {readFileSync}=require("node:fs");const {spawnSync}=require("node:child_process"); const [file,cli,b]=process.argv.slice(1); let by; try{by=JSON.parse(readFileSync(file,"utf8")).launch?.by}catch(e){console.log(`fail cannot read ${file}: ${e.code??"not JSON"}`);process.exit(0)} if(typeof by!=="string"||!by){console.log("fail the business file has no launch.by");process.exit(0)} const r=spawnSync(cli,["launches","list","--json"],{encoding:"utf8"}); if(r.status!==0){console.log(`fail launches list exited ${r.status}`);process.exit(0)} let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed no JSON");process.exit(0)} const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[]; console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);' check_f7() { local out rc=0 out="$(legacy_run pm-list 10 12 "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2> "$WORK/pm-list.err")" || rc=$? out="$(sed -n 1p <<< "$out")" if [ "$rc" = 0 ] && [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }" elif [ "$rc" != 0 ]; then ffail F7 pm-live "pm-list $(tester_capture_reason "$rc"); last: $(clean "$(tail -c 200 "$WORK/pm-list.err")")" else ffail F7 pm-live "$(clean "${out#fail }")"; fi } check_f8() { local s s="$(stat -c '%U %a %F' -- "$ROOT$CRED" 2>/dev/null)" || s="missing" if [ ! -L "$ROOT$CRED" ] && [ "$s" = "$S_USER 600 regular file" ]; then fpass F8 credential-file "$CRED is '$s'" else ffail F8 credential-file "$CRED is '$s', not '$S_USER 600 regular file'"; fi } # Empty when $1 and every directory above it, up to /, are the running user's (root's on the host) with # no group or other write: case2-unit.sh's root_only rule for --node, checked again before root runs it. # Otherwise the first path that breaks the rule. A symlink breaks it, because its mode is 777. not_root_only() { local p="$1" st while :; do st="$(stat -c '%u %a' -- "$ROOT$p" 2>/dev/null)" || { echo "$p (cannot stat)"; return; } [ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || { echo "$p (uid ${st% *}, mode ${st#* })"; return; } [ "$p" = / ] && return p="$(dirname -- "$p")" done } check_f9() { local s v bad s="$(stat -c '%U:%G %a %F' -- "$ROOT$SETTINGS" 2>/dev/null)" || s="missing" [ ! -L "$ROOT$SETTINGS" ] && [ "$s" = "root:root 644 regular file" ] || { ffail F9 autoupdater-off "$SETTINGS is '$s', not 'root:root 644 regular file'"; return; } # Root runs node here, so it runs only the pinned path, and only when nothing but root can replace it. [ "$NODE" = "/opt/mosaic-stack/node-$NODE_PIN/bin/node" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: it is not the --node-pin path"; return; } bad="$(not_root_only "$NODE")" [ -z "$bad" ] || { ffail F9 autoupdater-off "root won't run --node $NODE: $bad is not root's alone"; return; } v="$("$ROOT$NODE" -e 'const s=JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(JSON.stringify(s?.env?.DISABLE_AUTOUPDATER??null))' -- "$ROOT$SETTINGS" 2>/dev/null)" || v="unreadable" if [ "$v" = '"1"' ]; then fpass F9 autoupdater-off "$SETTINGS is root:root 0644 with env.DISABLE_AUTOUPDATER \"1\"" else ffail F9 autoupdater-off "$SETTINGS env.DISABLE_AUTOUPDATER is $v, not \"1\""; fi } check_f10() { local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v rc=0 [ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; } v="$(legacy_run node-version 5 7 "$NODE" --version 2> "$WORK/node-version.err")" || rc=$? v="$(sed -n 1p <<< "$v")" if [ "$rc" = 0 ] && [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v" elif [ "$rc" != 0 ]; then ffail F10 node-pin "node-version $(tester_capture_reason "$rc"); last: $(clean "$(tail -c 200 "$WORK/node-version.err")")" else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi } check_f11() { local d="$ROOT$DATA_ROOT/discord" found if [ -L "$d" ]; then ffail F11 no-discord "$DATA_ROOT/discord is a symlink"; return; fi [ -d "$d" ] || { fpass F11 no-discord "$DATA_ROOT/discord is absent"; return; } found="$(find "$d" -maxdepth 1 -name '*.json' -printf '%f ' 2>/dev/null)" if [ -z "$found" ]; then fpass F11 no-discord "$DATA_ROOT/discord holds no binding" else ffail F11 no-discord "$DATA_ROOT/discord holds bindings: ${found% }"; fi } # --- P checks --- check_p1() { local out out="$(sudo -l -U "$S_USER" 2>&1)" if grep -q "is not allowed to run sudo" <<< "$out"; then line P1 "$S_USER-sudo" closed "sudo -l: $(clean "$(sed -n 1p <<< "$out")")" else line P1 "$S_USER-sudo" unnamed "sudo -l: $(clean "$(tr '\n' ' ' <<< "$out")")"; fi } check_p2() { local gs keys="$ROOT$S_HOME/.ssh/authorized_keys" kstate n gs="$(groups_of "$S_USER" | tr '\n' ' ')"; gs="${gs% }" n="$(wc -w <<< "$gs")" if [ ! -e "$keys" ] && [ ! -L "$keys" ]; then kstate=absent elif [ -f "$keys" ] && [ ! -L "$keys" ] && [ ! -s "$keys" ]; then kstate=empty else kstate=present; fi if [ "$n" = 1 ] && [ "$kstate" != present ]; then line P2 "$S_USER-groups-keys" closed "id -G is '$gs'; authorized_keys is $kstate" else line P2 "$S_USER-groups-keys" unnamed "id -G is '$gs'; authorized_keys is $kstate"; fi } check_p3() { local n n="$(sudo -l -U localadmin 2>&1 | grep -c NOPASSWD)" line P3 localadmin-sudo "accepted Q183" "sudo -l -U localadmin lists $n NOPASSWD lines" } check_p4() { line P4 pve-root-pbs "accepted Q182+Q183" "root on pve over VM 1008, and PBS backups that hold $S_HOME with the credential; this script can't see pve"; } check_p5() { local gs reach if [ -z "$(uid_of jarvis)" ]; then line P5 jarvis-reach closed "no jarvis account"; return; fi gs="$(names_of_groups $(groups_of jarvis) | tr '\n' ' ')" reach="$(tr ' ' '\n' <<< "$gs" | grep -xE 'sudo|docker|lxd' | tr '\n' ' ')" if [ -z "$reach" ]; then line P5 jarvis-reach closed "jarvis is in none of sudo, docker and lxd (groups: ${gs% })" else line P5 jarvis-reach unnamed "jarvis is in ${reach% }"; fi } check_p6() { local g m members bad=0 for g in sudo docker lxd; do members="$(db group "$g" | cut -d: -f4)" for m in ${members//,/ }; do [ "$g" = sudo ] && [ "$m" = localadmin ] && continue line P6 "group-$g" unnamed "$m is a member of $g"; bad=1 done done [ "$bad" = 1 ] || line P6 group-members closed "sudo holds only localadmin, and docker and lxd have no members" } check_p7() { local found found="$(grep -rlw -- "$S_USER" "$ROOT/etc/sudoers.d" 2>/dev/null | sed "s#^$ROOT##" | tr '\n' ' ')" if [ -z "$found" ]; then line P7 sudoers-d closed "no file under /etc/sudoers.d names $S_USER" else line P7 sudoers-d unnamed "files naming $S_USER: ${found% }"; fi } check_p8() { local other other="$(listeners 7331)$(listeners 7333)" if loopback_by "$PORT" "$CON_PID" && [ -z "$other" ]; then line P8 lan-listeners closed "$PORT is on 127.0.0.1 only by MainPID $CON_PID; no TCP listener on 7331 or 7333; tester uses $TESTER_PATH" else line P8 lan-listeners unnamed "$PORT: $(listener_text "$PORT"); 7333: $(listener_text 7333); 7331: $(listener_text 7331); Console MainPID ${CON_PID:-none}" fi } check_p9() { local out n missing=() rc=0 err mg others out="$("$ROOT$HOSTCHECK" --check 2>/dev/null)" for n in 1 2 3 4 5 6; do grep -q "^ok $n " <<< "$out" || missing+=("hostcheck item $n has no ok line") done err="$(sudo -u jarvis /usr/bin/bwrap --version 2>&1 >/dev/null)" || rc=$? [ "$rc" != 0 ] && grep -q "Permission denied" <<< "$err" || missing+=("sudo -u jarvis /usr/bin/bwrap --version exited $rc without Permission denied") mg="$(gid_of "$S_USER")" [ -n "$mg" ] && [ -z "$(db group "$S_USER" | cut -d: -f4)" ] || missing+=("group $S_USER lists members, or $S_USER has no group") others="$(db passwd | awk -F: -v g="$mg" -v u="$S_USER" '$4 == g && $1 != u { print $1 }' | tr '\n' ' ')" [ -z "$others" ] || missing+=("other accounts have $S_USER's gid $mg: ${others% }") if [ ${#missing[@]} = 0 ]; then line P9 bwrap-userns "accepted mos-1096" "hostcheck items 1-6 ok, jarvis is denied bwrap, and group $S_USER has no other member" else for n in "${missing[@]}"; do line P9 bwrap-userns unnamed "$n"; done fi } # P10 is an observation of mutable same-uid state, never a closure ruling. TESTER_PEER='import socket,struct,sys s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) try: s.settimeout(1) s.connect(sys.argv[1]) p=struct.unpack("3i",s.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12)) print(*p) finally: s.close()' tester_snapshot() { local parent="${TESTER_PATH%/*}" p s pid [ ! -L "$ROOT$parent" ] && [ ! -L "$ROOT$TESTER_PATH" ] || return 1 p="$(stat -c '%u %a %F %d %i' -- "$ROOT$parent" 2>/dev/null)" || return 1 s="$(stat -c '%u %a %F %d %i' -- "$ROOT$TESTER_PATH" 2>/dev/null)" || return 1 [[ "$p" =~ ^$MOSAIC_UID\ 700\ directory\ [0-9]+\ [0-9]+$ ]] || return 1 [[ "$s" =~ ^$MOSAIC_UID\ 600\ socket\ [0-9]+\ [0-9]+$ ]] || return 1 pid="$(prop "$(unit_show "$CON_UNIT" MainPID)" MainPID)" [[ "$pid" =~ ^[1-9][0-9]*$ ]] && [ "$pid" = "$CON_PID" ] || return 1 [ "$(proc_uids "$pid")" = "$MOSAIC_UID $MOSAIC_UID $MOSAIC_UID $MOSAIC_UID" ] || return 1 printf '%s|%s|%s' "$pid" "$p" "$s" } tester_python() { local real bad magic # Resolve the system entry, then validate the target and every ancestor. [ -f "$ROOT/usr/bin/python3" ] && [ -x "$ROOT/usr/bin/python3" ] || return 1 real="$(realpath -e -- "$ROOT/usr/bin/python3" 2>/dev/null)" || return 1 [ -z "$ROOT" ] || real="${real#"$ROOT"}" case "$real" in /home/*|/root/*|/run/user/*|"$S_HOME"/*) return 1 ;; esac bad="$(not_root_only "$real")"; [ -z "$bad" ] || return 1 bad="$(not_root_only /usr/bin)"; [ -z "$bad" ] || return 1 magic="$(od -An -tx1 -N4 -- "$ROOT$real" 2>/dev/null | tr -d ' \n')" [ "$magic" = 7f454c46 ] || return 1 printf '%s' "$real" } check_p10() { local floor before after python peer pid uid gid why="" pattern='^([1-9][0-9]{0,9}) ([0-9]{1,10}) ([0-9]{1,10})$' floor="$(cat "$ROOT/proc/sys/net/ipv4/ip_unprivileged_port_start" 2>/dev/null)" [[ "$floor" =~ ^[0-9]{1,5}$ ]] && [ "$floor" -gt "$PORT" ] || why=floor before="$(tester_snapshot)" || why=metadata python="$(tester_python)" || why=interpreter if [ -z "$why" ]; then peer="$(tester_capture 2 4 128 "$S_USER" "$S_HOME" "$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$python" -I -S -c "$TESTER_PEER" "$TESTER_PATH")" || why="probe-$(tester_capture_reason "$?")" if [[ "$peer" =~ $pattern ]]; then pid="${BASH_REMATCH[1]}" uid="${BASH_REMATCH[2]}" gid="${BASH_REMATCH[3]}" [ "$pid" = "$CON_PID" ] && [ "$uid" = "$MOSAIC_UID" ] && [ "$gid" -le 2147483647 ] || why=peer else [ -n "$why" ] || why=probe; fi after="$(tester_snapshot)" || why=metadata [ "$before" = "$after" ] || why=changed fi if [ -z "$why" ]; then line P10 tester-socket-squat unnamed "boundary=observed path=$TESTER_PATH parent=0700 owner=$MOSAIC_UID socket=0600 peer_pid=$pid peer_uid=$uid floor=$floor port=$PORT; hostile same-uid substitution remains" else line P10 tester-socket-squat unnamed "boundary=failed reason=$why path=$TESTER_PATH MainPID=${CON_PID:-none}; hostile same-uid substitution remains" fi } check_p11() { local en before en="$(systemctl is-enabled mosaic-hostcheck.service 2>/dev/null)" before="$(prop "$(unit_show mosaic-hostcheck.service Before)" Before)" if [ "$en" = enabled ] && grep -qxF -- "$BUS_UNIT" <<< "${before// /$'\n'}"; then line P11 hostcheck-before-bus closed "mosaic-hostcheck.service is enabled, and its Before= names $BUS_UNIT" else line P11 hostcheck-before-bus unnamed "mosaic-hostcheck.service is '${en:-unknown}', Before='$before'; it needs enabled and $BUS_UNIT" fi } check_f1 check_f2 F5_LINE=() check_f3_f5 check_f4 "${F5_LINE[0]}" F5 bwrap-label "${F5_LINE[@]:1}" check_f6 check_f7 check_f8 check_f9 check_f10 check_f11 check_p1; check_p2; check_p3; check_p4; check_p5; check_p6 check_p7; check_p8; check_p9; check_p10; check_p11 for m in "${MARKS[@]}"; do echo "count $m ${COUNT[$m]:-0}"; done if [ "$F_FAILED" = 0 ]; then echo "case1-accept: every F check passes; send the whole output to Duckworth, and each unnamed line to Mos"; exit 0; fi echo "case1-accept: an F check failed, so the row fails" exit 1