Skip to main content

REMOTE_PROCESS_IDENTITY_SCRIPT

Constant REMOTE_PROCESS_IDENTITY_SCRIPT 

Source
pub const REMOTE_PROCESS_IDENTITY_SCRIPT: &str = "# Linux process-group identity shared by launch, timeout cleanup, and recovery.\n# A PID is not durable authority: bind it to the worker boot and leader start.\nset -f\n\nrch_valid_pgid() {\n    case \"$1\" in \'\'|*[!0-9]*|0*|1) return 1;; esac\n    [ \"${#1}\" -le 10 ] && [ \"$1\" -le 2147483647 ]\n}\n\nrch_valid_boot() {\n    case \"$1\" in *[!0-9a-f-]*) return 1;; esac\n    case \"$1\" in ????????-????-????-????-????????????) return 0;; esac\n    return 1\n}\n\n# /proc can belong to an ancestor PID namespace while kill uses ours. Never\n# interpret that foreign view as ownership, or as proof that a group is empty.\n# NSpid lists IDs from procfs\'s namespace through our active namespace. Require\n# exactly our own ID: equality with just the first ID can occur by coincidence.\n# Use builtin read so /proc/self describes this shell, not a spawned reader.\n# Missing namespace evidence (including older kernels) fails closed.\n# Inside ( ... ) $$ still names the invoking shell while /proc/self is this\n# subshell (the watchdog\'s timer is one), so compare with this process\'s own\n# ID: a command exec\'d from a substitution reports it as its parent. Use an\n# absolute path: launch environments may carry a minimal PATH.\nrch_check_procfs_namespace() {\n    rch_self_pid=$(exec /bin/sh -c \'echo \"$PPID\"\') || return 1\n    while IFS= read -r rch_status_line; do\n        case \"$rch_status_line\" in\n            NSpid:*)\n                set -- ${rch_status_line#NSpid:}\n                [ \"$#\" -eq 1 ] && [ \"$1\" = \"$rch_self_pid\" ]\n                return $?;;\n        esac\n    done < /proc/self/status 2>/dev/null\n    return 1\n}\n\n# Read a member of the bound group, including comm with spaces or parentheses.\n# A member may be a non-leader TID: /proc/<tid>/stat has the same birth/group\n# fields. This preserves escalation when a process leader is already a zombie\n# but another thread of that process is still running.\n# Function positional arguments do not replace the launcher\'s command arguments.\nrch_read_process() {\n    rch_valid_pgid \"$1\" && rch_check_procfs_namespace || return 1\n    rch_proc_pid=$1\n    rch_observed_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 1\n    rch_valid_boot \"$rch_observed_boot\" || return 1\n    rch_stat=$(cat \"/proc/$rch_proc_pid/stat\" 2>/dev/null) || return 1\n    case \"$rch_stat\" in \"$rch_proc_pid (\"*\") \"*) ;; *) return 1;; esac\n    rch_stat=${rch_stat##*) }\n    set -- $rch_stat\n    [ \"$#\" -ge 20 ] && [ \"$3\" = \"$rch_pgid\" ] || return 1\n    # Keep the same conservative state grammar as process_identity.rs. An\n    # unknown future state is not authority to signal a process group.\n    case \"$1\" in R|S|D|T|t|K|W|P|I) ;; *) return 1;; esac\n    shift 19\n    rch_observed_start=$1\n    case \"$rch_observed_start\" in \'\'|*[!0-9]*|0*) return 1;; esac\n    [ \"${#rch_observed_start}\" -le 20 ]\n}\n\nrch_read_leader() {\n    rch_read_process \"$rch_pgid\"\n}\n\nrch_remote_leader_matches() {\n    rch_read_leader || return 1\n    [ \"$rch_observed_boot\" = \"$rch_boot\" ] && [ \"$rch_observed_start\" = \"$rch_start\" ]\n}\n\n# Publish before starting any workload. A complete record appears in one rename.\n# The caller must already be the session/process-group leader created by setsid.\nrch_remote_record() {\n    rch_pgid=$$\n    rch_valid_pgid \"$rch_pgid\" && rch_read_leader || return 1\n    rch_boot=$rch_observed_boot\n    rch_start=$rch_observed_start\n    # Refuse before publishing execution authority or spawning the payload.\n    # Scope placement moves this leader, not the workload into a separate tree.\n    rch_apply_configured_worker_policy || return 125\n    rch_record_tmp=\"$1.$rch_pgid\"\n    (umask 077; set -C; printf \'RCH_REMOTE_PROCESS_V1:%s:%s:%s:%s\\n\' \\\n        \"$2\" \"$rch_boot\" \"$rch_pgid\" \"$rch_start\" > \"$rch_record_tmp\") || return 1\n    mv -f \"$rch_record_tmp\" \"$1\"\n}\n\nrch_read_record() {\n    case \"$1\" in /*) ;; *) return 42;; esac\n    [ -f \"$1\" ] && [ ! -L \"$1\" ] && [ -r \"$1\" ] || return 42\n    rch_record=$(cat \"$1\" 2>/dev/null) || return 42\n    rch_identified=0\n    case \"$rch_record\" in\n        RCH_REMOTE_PROCESS_V1:*)\n            rch_fields=${rch_record#RCH_REMOTE_PROCESS_V1:}\n            rch_record_build=${rch_fields%%:*}\n            [ \"$rch_record_build\" = \"$2\" ] || return 43\n            rch_fields=${rch_fields#*:}\n            rch_boot=${rch_fields%%:*}\n            rch_fields=${rch_fields#*:}\n            rch_pgid=${rch_fields%%:*}\n            rch_start=${rch_fields#*:}\n            rch_valid_boot \"$rch_boot\" || return 42\n            case \"$rch_start\" in \'\'|*[!0-9]*|0*) return 42;; esac\n            [ \"${#rch_start}\" -le 20 ] || return 42\n            [ \"$rch_record\" = \"RCH_REMOTE_PROCESS_V1:$2:$rch_boot:$rch_pgid:$rch_start\" ] || return 42\n            # Command substitution strips trailing newlines (and some shells\n            # discard NULs). Require the original bytes to be one complete line.\n            rch_record_bytes=$(LC_ALL=C wc -c < \"$1\") || return 42\n            rch_record_lines=$(LC_ALL=C wc -l < \"$1\") || return 42\n            [ \"$rch_record_bytes\" -eq \"$((${#rch_record} + 1))\" ] && \\\n                [ \"$rch_record_lines\" -eq 1 ] || return 42\n            rch_identified=1\n            ;;\n        *) rch_pgid=$rch_record;;\n    esac\n    rch_valid_pgid \"$rch_pgid\" || return 42\n}\n\n# 0: no executing members; 1: live members; 2: observation unavailable.\n# kill -0 cannot distinguish ESRCH from EPERM, or live processes from zombies.\n# A zombie process leader does not prove its other threads have exited. Enumerate\n# LWPs, retaining their own IDs for live escalation witnesses. A terminal-only\n# snapshot must account for every reported thread; unsupported/incomplete thread\n# enumeration is unknown, never a fallback to a process-only empty-group claim.\nrch_group_state() {\n    rch_check_procfs_namespace || return 2\n    rch_task_snapshot=$(LC_ALL=C ps -e -L -o pid= -o lwp= -o pgid= -o stat= -o nlwp=) || return 2\n    rch_snapshot=$(printf \'%s\\n\' \"$rch_task_snapshot\" | LC_ALL=C awk -v group=\"$rch_pgid\" -v observer=\"$$\" \'\n        NF != 5 || $1 !~ /^[1-9][0-9]*$/ || $2 !~ /^[1-9][0-9]*$/ || $3 !~ /^[0-9]+$/ || $4 !~ /^[RSDTtKWPIXZx][A-Za-z0-9+<>-]*$/ || $5 !~ /^[1-9][0-9]*$/ { bad = 1; next }\n        seen_task[$2]++ { bad = 1 }\n        $2 == observer { seen = 1; if ($3 == group) bad = 1 }\n        $3 == group {\n            members[$1]++\n            if ($1 in expected && expected[$1] != $5) uncertain = 1\n            expected[$1] = $5\n            if ($4 !~ /^[ZXx]/) live = 1\n        }\n        { print $2, $3, $4 }\n        END {\n            if (bad || !seen) exit 2\n            # Positive liveness needs no complete census. Requiring a stable\n            # thread count here would prevent cancelling a busy compiler.\n            if (live) exit 1\n            for (pid in members) if (members[pid] != expected[pid]) uncertain = 1\n            if (uncertain) exit 2\n            exit 0\n        }\n    \')\n    return $?\n}\n\n# Capture live members while the recorded leader still authenticates the group.\n# TERM can kill that leader before a child that ignores TERM. A surviving\n# member\'s birth identity lets THIS cancellation finish escalation without\n# treating a historical PGID as authority. These witnesses are never persisted.\nrch_capture_group_witnesses() {\n    rch_witnesses=\n    rch_witness_identity=\n    [ \"$rch_identified\" -eq 1 ] && rch_remote_leader_matches || return 43\n    # rch_group_state normalized the validated LWP census to ID/group/state.\n    # Retain live TIDs, not just process leaders, with the same birth fencing.\n    rch_member_pids=$(printf \'%s\\n\' \"$rch_snapshot\" | LC_ALL=C awk -v group=\"$rch_pgid\" \'\n        $2 == group && $3 !~ /^[ZXx]/ { print $1 }\n    \') || return 44\n    for rch_member_pid in $rch_member_pids; do\n        if rch_read_process \"$rch_member_pid\" && [ \"$rch_observed_boot\" = \"$rch_boot\" ]; then\n            rch_witnesses=\"$rch_witnesses $rch_member_pid:$rch_observed_start\"\n        fi\n    done\n    # Do not transfer authority across a leader exit/reuse during collection.\n    rch_remote_leader_matches || { rch_witnesses=; return 43; }\n    rch_witness_identity=\"$rch_boot:$rch_pgid:$rch_start\"\n}\n\nrch_remote_witness_matches() {\n    [ \"${rch_witness_identity:-}\" = \"$rch_boot:$rch_pgid:$rch_start\" ] || return 1\n    for rch_witness in $rch_witnesses; do\n        rch_witness_pid=${rch_witness%%:*}\n        rch_witness_start=${rch_witness#*:}\n        # rch_read_process also checks current group membership and liveness.\n        if rch_read_process \"$rch_witness_pid\" &&\n            [ \"$rch_observed_boot\" = \"$rch_boot\" ] &&\n            [ \"$rch_observed_start\" = \"$rch_witness_start\" ]; then\n            return 0\n        fi\n    done\n    return 1\n}\n\n# Return 43 for unknown/reused identity. A pre-existing orphan is never adopted;\n# only an authenticated TERM in this call can authorize witnessed escalation.\nrch_signal_group() {\n    [ \"$rch_identified\" -eq 1 ] || return 43\n    if ! rch_remote_leader_matches; then\n        [ \"$1\" = KILL ] && [ \"${rch_term_sent:-0}\" -eq 1 ] &&\n            rch_remote_witness_matches || return 43\n    fi\n    # Revalidate immediately before EACH signal. dash requires no `--` here.\n    kill -\"$1\" -\"$rch_pgid\" 2>/dev/null || :\n}\n\nrch_remote_cancel() {\n    # No authority may leak between calls, even when parsing the next record fails.\n    rch_witnesses=\n    rch_witness_identity=\n    rch_term_sent=0\n    rch_read_record \"$1\" \"$2\" || return $?\n    rch_mode=$3\n    rch_group_state\n    case $? in 0) return 0;; 1) ;; *) return 44;; esac\n    if [ \"$rch_mode\" = term ]; then\n        rch_capture_group_witnesses || return $?\n        rch_signal_group TERM || return $?\n        rch_term_sent=1\n        sleep 1 || return 44\n        rch_group_state\n        case $? in 0) return 0;; 1) ;; *) return 44;; esac\n    fi\n    rch_signal_group KILL || return $?\n    rch_attempt=0\n    while [ \"$rch_attempt\" -lt 20 ]; do\n        rch_group_state\n        case $? in 0) return 0;; 1) ;; *) return 44;; esac\n        rch_attempt=$((rch_attempt + 1))\n        sleep 0.1 || return 44\n    done\n    return 45\n}\n\n# Optional receiver-side execution policy for tracked Linux build payloads.\n# These functions travel with the launch protocol; no worker launcher is needed.\n# Sync, pre-launch package bootstrap, and untracked execution are NOT confined\n# here. This is not by itself admission for a dual-role worker (GH #104).\n#\n# Configure ONE of /etc/rch/worker-execution.conf (root owned) or\n# $HOME/.config/rch/worker-execution.conf (owned by the SSH user or root):\n#   version=1\n#   nice=10\n#   systemd_slice=jobs.slice\n#   systemd_manager=user\n# All specified settings are required. Missing policy preserves legacy behavior.\n# Unknown/duplicate keys, an unreadable file, or failed enforcement refuse setup.\n# No policy contents are ever evaluated as shell code.\n\nrch_policy_refuse() {\n    printf \'RCH_WORKER_POLICY_REFUSED:%s\\n\' \"$1\" >&2\n    return 125\n}\n\nrch_policy_valid_nice() {\n    case \"$1\" in 0|[1-9]|1[0-9]|-[1-9]|-1[0-9]|-20) return 0;; esac\n    return 1\n}\n\nrch_policy_valid_slice() {\n    case \"$1\" in *.slice) rch_policy_slice_base=${1%.slice};; *) return 1;; esac\n    [ -n \"$rch_policy_slice_base\" ] && [ \"${#1}\" -le 200 ] || return 1\n    case \"$rch_policy_slice_base\" in\n        -*|*-|*--*|*[!a-zA-Z0-9_-]*) return 1;;\n    esac\n}\n\nrch_policy_select_file() {\n    rch_policy_file=\n    rch_policy_uid=$(id -u) || { rch_policy_refuse uid; return 125; }\n    rch_policy_system=/etc/rch/worker-execution.conf\n    rch_policy_user=\n    case \"${HOME:-}\" in /*) rch_policy_user=\"$HOME/.config/rch/worker-execution.conf\";; esac\n    # An inaccessible containing directory is not evidence of absent policy.\n    for rch_policy_dir in /etc/rch \"${HOME:-/}/.config\" \"${HOME:-/}/.config/rch\"; do\n        if [ -d \"$rch_policy_dir\" ] && [ ! -x \"$rch_policy_dir\" ]; then\n            rch_policy_refuse unreadable_directory; return 125\n        fi\n    done\n    if [ -e \"$rch_policy_system\" ] || [ -L \"$rch_policy_system\" ]; then\n        rch_policy_file=$rch_policy_system\n        rch_policy_owner=0\n    fi\n    if [ -n \"$rch_policy_user\" ] && { [ -e \"$rch_policy_user\" ] || [ -L \"$rch_policy_user\" ]; }; then\n        [ -z \"$rch_policy_file\" ] || { rch_policy_refuse multiple_policies; return 125; }\n        rch_policy_file=$rch_policy_user\n        rch_policy_owner=$rch_policy_uid\n    fi\n}\n\n# Parse the entire bounded, ASCII data file before changing process attributes.\n# owner is the expected uid; root-owned user policy is also accepted.\nrch_policy_read_file() {\n    rch_policy_nice=\n    rch_policy_slice=\n    rch_policy_manager=user\n    rch_policy_seen=\':\'\n    [ -f \"$1\" ] && [ ! -L \"$1\" ] && [ -r \"$1\" ] || {\n        rch_policy_refuse unreadable_file; return 125;\n    }\n    rch_policy_stat=$(LC_ALL=C stat -c \'%u:%a:%s:%h\' -- \"$1\") || {\n        rch_policy_refuse file_metadata; return 125;\n    }\n    rch_policy_file_uid=${rch_policy_stat%%:*}\n    rch_policy_stat=${rch_policy_stat#*:}\n    rch_policy_mode=${rch_policy_stat%%:*}\n    rch_policy_stat=${rch_policy_stat#*:}\n    rch_policy_size=${rch_policy_stat%%:*}\n    rch_policy_links=${rch_policy_stat#*:}\n    case \"$rch_policy_mode\" in [0-7][0-7][0-7]) ;; *) rch_policy_refuse file_mode; return 125;; esac\n    [ \"$rch_policy_file_uid\" = \"$2\" ] || [ \"$rch_policy_file_uid\" = 0 ] || {\n        rch_policy_refuse file_owner; return 125;\n    }\n    [ \"$((0$rch_policy_mode & 022))\" -eq 0 ] && [ \"$rch_policy_links\" = 1 ] || {\n        rch_policy_refuse writable_or_linked_file; return 125;\n    }\n    [ \"$rch_policy_size\" -le 8192 ] || { rch_policy_refuse file_size; return 125; }\n    # Shell read can discard NULs. Validate bytes before parsing, not afterwards.\n    LC_ALL=C tr -cd \'\\11\\12\\40-\\176\' < \"$1\" | cmp -s - \"$1\" || {\n        rch_policy_refuse file_encoding; return 125;\n    }\n    while IFS= read -r rch_policy_line || [ -n \"$rch_policy_line\" ]; do\n        case \"$rch_policy_line\" in \'\'|\'#\'*) continue;; esac\n        case \"$rch_policy_line\" in *=*) ;; *) rch_policy_refuse syntax; return 125;; esac\n        rch_policy_key=${rch_policy_line%%=*}\n        rch_policy_value=${rch_policy_line#*=}\n        case \"$rch_policy_seen\" in *\":$rch_policy_key:\"*) rch_policy_refuse duplicate_key; return 125;; esac\n        rch_policy_seen=\"$rch_policy_seen$rch_policy_key:\"\n        case \"$rch_policy_key\" in\n            version) [ \"$rch_policy_value\" = 1 ] || { rch_policy_refuse version; return 125; };;\n            nice)\n                rch_policy_valid_nice \"$rch_policy_value\" || { rch_policy_refuse nice; return 125; }\n                rch_policy_nice=$rch_policy_value;;\n            systemd_slice)\n                rch_policy_valid_slice \"$rch_policy_value\" || { rch_policy_refuse slice; return 125; }\n                rch_policy_slice=$rch_policy_value;;\n            systemd_manager)\n                case \"$rch_policy_value\" in user|system) ;; *) rch_policy_refuse manager; return 125;; esac\n                rch_policy_manager=$rch_policy_value;;\n            *) rch_policy_refuse unknown_key; return 125;;\n        esac\n    done < \"$1\"\n    case \"$rch_policy_seen\" in *:version:*) ;; *) rch_policy_refuse missing_version; return 125;; esac\n    [ -n \"$rch_policy_nice$rch_policy_slice\" ] || { rch_policy_refuse empty_policy; return 125; }\n    case \"$rch_policy_seen\" in\n        *:systemd_manager:*)\n            [ -n \"$rch_policy_slice\" ] || { rch_policy_refuse manager_without_slice; return 125; };;\n    esac\n}\n\nrch_policy_systemctl() {\n    timeout 5s systemctl \"--$rch_policy_manager\" --no-pager --no-ask-password \"$@\"\n}\n\n# Read the launcher\'s actual unified cgroup. A successful D-Bus job submission\n# is not evidence that systemd moved it; cgroup v1/ambiguous evidence is refused.\nrch_policy_current_cgroup() (\n    rch_policy_cgroup_count=0\n    rch_policy_cgroup_value=\n    while IFS= read -r rch_policy_cgroup_line; do\n        case \"$rch_policy_cgroup_line\" in\n            0::/*)\n                rch_policy_cgroup_count=$((rch_policy_cgroup_count + 1))\n                rch_policy_cgroup_value=${rch_policy_cgroup_line#0::};;\n        esac\n    done < \"/proc/$$/cgroup\"\n    [ \"$rch_policy_cgroup_count\" -eq 1 ] || exit 125\n    printf \'%s\\n\' \"$rch_policy_cgroup_value\"\n)\n\nrch_policy_enter_scope() {\n    command -v busctl >/dev/null 2>&1 && command -v systemctl >/dev/null 2>&1 &&\n        command -v timeout >/dev/null 2>&1 || { rch_policy_refuse systemd_tools; return 125; }\n    # An operator must start/configure the selected slice. Do not synthesize a\n    # missing slice and accidentally run without its intended resource limits.\n    [ \"$(rch_policy_systemctl show --value --property=ActiveState -- \"$rch_policy_slice\")\" = active ] || {\n        rch_policy_refuse slice_not_active; return 125;\n    }\n    rch_policy_slice_cgroup=$(rch_policy_systemctl show --value --property=ControlGroup -- \"$rch_policy_slice\") || {\n        rch_policy_refuse slice_cgroup; return 125;\n    }\n    case \"$rch_policy_slice_cgroup\" in /*) ;; *) rch_policy_refuse slice_cgroup; return 125;; esac\n    [ \"$rch_policy_slice_cgroup\" != / ] || { rch_policy_refuse root_slice; return 125; }\n    rch_policy_scope=\"rch-$rch_boot-$rch_pgid-$rch_start.scope\"\n    # Attach the EXISTING setsid leader, rather than starting an independent\n    # transient service. All subsequently spawned work keeps the same PGID and\n    # stdio, so the existing watchdog, cancellation and recovery remain owners.\n    busctl \"--$rch_policy_manager\" --timeout=5s --allow-interactive-authorization=no \\\n        call org.freedesktop.systemd1 /org/freedesktop/systemd1 \\\n        org.freedesktop.systemd1.Manager StartTransientUnit \'ssa(sv)a(sa(sv))\' \\\n        \"$rch_policy_scope\" fail 3 PIDs au 1 \"$rch_pgid\" \\\n        Slice s \"$rch_policy_slice\" Description s \'RCH tracked build\' 0 >/dev/null || {\n        rch_policy_refuse scope_creation; return 125;\n    }\n    rch_policy_expected_cgroup=\"$rch_policy_slice_cgroup/$rch_policy_scope\"\n    rch_policy_poll=0\n    while [ \"$(rch_policy_current_cgroup)\" != \"$rch_policy_expected_cgroup\" ]; do\n        rch_policy_poll=$((rch_policy_poll + 1))\n        [ \"$rch_policy_poll\" -lt 50 ] || { rch_policy_refuse scope_not_enforced; return 125; }\n        sleep 0.1 || { rch_policy_refuse scope_wait; return 125; }\n    done\n    [ \"$(rch_policy_systemctl show --value --property=ActiveState -- \"$rch_policy_scope\")\" = active ] &&\n        [ \"$(rch_policy_systemctl show --value --property=Slice -- \"$rch_policy_scope\")\" = \"$rch_policy_slice\" ] &&\n        [ \"$(rch_policy_systemctl show --value --property=ControlGroup -- \"$rch_policy_scope\")\" = \"$rch_policy_expected_cgroup\" ] || {\n        rch_policy_refuse scope_evidence; return 125;\n    }\n}\n\nrch_policy_enforce() {\n    [ \"$(uname -s)\" = Linux ] || { rch_policy_refuse platform; return 125; }\n    rch_remote_leader_matches || { rch_policy_refuse process_identity; return 125; }\n    if [ -n \"$rch_policy_nice\" ]; then\n        # util-linux -n becomes relative with POSIXLY_CORRECT. Clear that flag\n        # only in the helper process; never change the build\'s environment.\n        (unset POSIXLY_CORRECT; renice -n \"$rch_policy_nice\" -p \"$rch_pgid\") >/dev/null || {\n            rch_policy_refuse priority_change; return 125;\n        }\n        rch_policy_observed_nice=$(LC_ALL=C ps -o ni= -p \"$rch_pgid\") || {\n            rch_policy_refuse priority_observation; return 125;\n        }\n        # ps pads the numeric field; splitting is safe after the numeric check.\n        case \"$rch_policy_observed_nice\" in *[!\\ 0-9-]*) rch_policy_refuse priority_observation; return 125;; esac\n        set -- $rch_policy_observed_nice\n        [ \"$#\" -eq 1 ] && [ \"$1\" = \"$rch_policy_nice\" ] || {\n            rch_policy_refuse priority_not_enforced; return 125;\n        }\n    fi\n    rch_policy_scope=\n    if [ -n \"$rch_policy_slice\" ]; then\n        rch_policy_enter_scope || return 125\n    fi\n    rch_remote_leader_matches || { rch_policy_refuse process_identity; return 125; }\n    printf \'RCH_WORKER_EXECUTION_POLICY:nice=%s,slice=%s,manager=%s,scope=%s\\n\' \\\n        \"${rch_policy_nice:-inherit}\" \"${rch_policy_slice:-none}\" \\\n        \"$rch_policy_manager\" \"${rch_policy_scope:-none}\" >&2\n}\n\nrch_apply_configured_worker_policy() {\n    rch_policy_select_file || return 125\n    [ -n \"$rch_policy_file\" ] || return 0\n    rch_policy_read_file \"$rch_policy_file\" \"$rch_policy_owner\" || return 125\n    rch_policy_enforce\n}\n";
Expand description

Shared Linux launch and cancellation protocol; callers select their receipt.