Keep USB approvals portable and preserve approval prompts

This commit is contained in:
acrogenesis committed 2026-09-29 10:16:56 -06:00
1 parent 60d6b255da
commit c3cdcb1e82
15 files changed
+535 -52

No files matched your search

+12 -9
View File
@@ -60,19 +60,22 @@ fi
# fails, the command stops here and leaves the complete policy enabled.
sudo omarchy-usb-authorization-boot disable
if omarchy-pkg-present usbguard; then
# Keep the watcher and its IPC access until blocking has actually stopped.
# In particular, a failed stop must not strand a running daemon without UI.
sudo systemctl disable --now usbguard.service
if systemctl is-active --quiet usbguard.service || systemctl is-enabled --quiet usbguard.service; then
echo "USBGuard is still active or enabled; approval prompts have been preserved." >&2
exit 1
fi
sudo omarchy-usb-authorization-restore-default
sudo usbguard remove-user "$USER"
fi
systemctl --user disable --now omarchy-usb-authorization.service 2>/dev/null || true
rm -f "$unit"
systemctl --user daemon-reload
if omarchy-pkg-present usbguard; then
sudo usbguard remove-user "$USER" 2>/dev/null || true
fi
if omarchy-pkg-present usbguard; then
sudo systemctl disable --now usbguard.service 2>/dev/null || true
sudo omarchy-usb-authorization-restore-default
fi
if [[ -d $state_dir/requests ]]; then
find "$state_dir/requests" -mindepth 1 -maxdepth 1 -type f -delete
rmdir "$state_dir/requests" 2>/dev/null || true
+75 -9
View File
@@ -1,20 +1,22 @@
#!/bin/bash
# omarchy:summary=Ask before new USB devices can access the system
# omarchy:args=[--boot] [--yes]
# omarchy:args=[--boot] [--yes] [--review-existing]
# omarchy:requires-sudo=true
set -euo pipefail
assume_yes=0
from_boot=0
review_existing=0
while (( $# > 0 )); do
case "$1" in
--yes) assume_yes=1 ;;
--boot) from_boot=1 ;;
--review-existing) review_existing=1 ;;
-h | --help)
echo "Usage: omarchy-setup-security-usb-authorization [--boot] [--yes]"
echo "Usage: omarchy-setup-security-usb-authorization [--boot] [--yes] [--review-existing]"
echo
echo "Trusts USB devices that are connected now, then blocks new devices"
echo "until you approve them from an Omarchy notification."
@@ -25,6 +27,9 @@ while (( $# > 0 )); do
echo "Older snapshots without USBGuard keep all USB disabled after startup,"
echo "including keyboards, network adapters, and storage. Use --boot only if"
echo "disk unlock and recovery work without any USB devices."
echo
echo "--review-existing reviews connected devices with older port-bound approvals."
echo "Each portable approval requires confirmation; existing rules are preserved."
exit 0
;;
*)
@@ -43,6 +48,58 @@ daemon_config=/etc/usbguard/usbguard-daemon.conf
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
if (( review_existing )); then
# Older Omarchy rules have no per-rule ownership marker. They cannot safely
# be distinguished from intentionally restricted administrator rules.
(( ! assume_yes && ! from_boot )) || { echo "Review requires individual confirmation; do not combine it with --yes or --boot." >&2; exit 2; }
policy=$(usbguard list-rules)
declare -A reviewed=()
# Keep stdin attached to the terminal for gum (and any sudo prompt).
mapfile -t policy_lines <<<"$policy"
for line in "${policy_lines[@]}"; do
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
rule=${line#*: }
[[ $rule == allow\ * ]] || continue
portable=$(usb_authorization_portable_rule "$rule") || continue
[[ ${portable% label *} != "$rule" ]] || continue
devices=$(usbguard list-devices "$rule")
mapfile -t candidates <<<"$devices"
for device in "${candidates[@]}"; do
[[ $device =~ ^([0-9]+):[[:space:]](allow[[:space:]].*)$ ]] || continue
id=${BASH_REMATCH[1]} current=${BASH_REMATCH[2]}
[[ ! -v reviewed[$id] ]] || continue
reviewed[$id]=1
echo
printf '%s\n' "$current" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
echo "This device has a location-bound approval. Details are supplied by the device."
if gum confirm "Trust this device on any USB port or hub?"; then
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "$current"
fi
done
fi
done
echo "Review complete. Disconnected devices can be approved when reconnected."
exit 0
fi
# The watcher runs in the session's environment, which may differ from a
# command invoked in a checkout. Fail before enabling blocking on mismatched
# or incomplete installations, including dangling dev-link unit symlinks.
session_environment=$(systemctl --user show-environment)
grep -Fqx -- "OMARCHY_PATH=$OMARCHY_PATH" <<<"$session_environment" || {
echo "The session and this command use different Omarchy installations. Update the session before enabling USB protection." >&2
exit 1
}
for required in "$unit_source" /usr/share/omarchy/install/helpers/usb-authorization-policy.sh; do
[[ -r $required ]] || { echo "USB authorization installation is incomplete: $required" >&2; exit 1; }
done
for required in "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-review" \
/usr/bin/omarchy-usb-authorization-approve; do
[[ -r $required && -x $required ]] || { echo "USB authorization command is unavailable: $required" >&2; exit 1; }
done
if (( ! assume_yes )); then
if (( from_boot )); then
gum style --foreground 212 --bold "USB authorization from boot"
@@ -93,19 +150,27 @@ sudo usbguard add-user "$USER" \
--policy=list \
--exceptions=listen
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
# Install a real file: a link into a checkout can become dangling on a branch
# switch. Start the waiting watcher before USBGuard can block new devices.
unit_temporary=$(mktemp "${unit}.XXXXXXXXXX")
install -m644 "$unit_source" "$unit_temporary"
mv -fT "$unit_temporary" "$unit"
systemctl --user daemon-reload
systemctl --user enable omarchy-usb-authorization.service
systemctl --user restart omarchy-usb-authorization.service
systemctl --user is-active --quiet omarchy-usb-authorization.service
sudo systemctl enable usbguard.service
# add-user ACL changes are loaded only when the daemon starts, so restart an
# existing service as well as starting a fresh one.
sudo systemctl restart usbguard.service
ln -sfn "$unit_source" "$unit"
systemctl --user daemon-reload
systemctl --user enable --now omarchy-usb-authorization.service
systemctl is-active --quiet usbguard.service
systemctl --user is-active --quiet omarchy-usb-authorization.service
if (( from_boot )); then
# Take the trust snapshot only after USBGuard is enforcing the policy. Every
# ID is resolved by the daemon, and no device-controlled descriptor text is
# passed to a privileged command.
# The fixed helper independently verifies each full live device snapshot.
usb_authorization_trust_present_devices
sudo omarchy-usb-authorization-boot enable
fi
@@ -113,6 +178,7 @@ fi
echo
echo "USB device authorization is enabled."
echo "Devices connected during setup remain trusted; new devices require approval."
echo "To review older port-bound approvals, run: omarchy-setup-security-usb-authorization --review-existing"
if (( from_boot )); then
echo "Boot-time authorization is enabled and takes effect after reboot."
fi
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash -p
# omarchy:hidden=true
# omarchy:summary=Save portable trust for one verified USB device
# This narrow Polkit boundary accepts a live device snapshot, never a policy or
# file path. Privileged Bash suppresses startup code from the caller's environment.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset USBGUARD_DAEMON_CONF USBGUARD_DEBUG
source /usr/share/omarchy/install/helpers/usb-authorization-policy.sh
(( $# == 2 )) || exit 64
# Omarchy persists to this file. Refuse custom daemon storage rather than
# mistaking a stale copy of a rule for a successful persistent decision.
rules_file=$(awk -F= '/^[[:space:]]*RuleFile[[:space:]]*=/ { value=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); print value; exit }' /etc/usbguard/usbguard-daemon.conf)
[[ $rules_file == "/etc/usbguard/rules.conf" ]] || exit 1
exec 8>/run/omarchy-usb-authorization.lock
flock -x 8
usb_authorization_permanent "$1" "$2"
+5 -7
View File
@@ -98,13 +98,11 @@ case "$choice" in
jq --arg approval "$choice" '.approval = $approval' "$request" >"$temporary"
mv -f "$temporary" "$request"
if [[ $current_rule == "$expected_rule" ]]; then
if [[ $choice == "Always allow this device" ]]; then
pkexec /usr/bin/omarchy-usb-authorization-approve "$id" "$current_rule" || approval_failed
elif [[ $current_rule == "$expected_rule" ]]; then
# Match the complete identity, since numeric IDs can be reassigned.
options=()
if [[ $choice == "Always allow this device" ]]; then
options+=(--permanent)
fi
usbguard allow-device "${options[@]}" "$expected_rule" || approval_failed
usbguard allow-device "$expected_rule" || approval_failed
fi
# Rule-based USBGuard commands swallow applyDevicePolicy exceptions and can
@@ -115,7 +113,7 @@ case "$choice" in
if [[ $choice == "Always allow this device" ]]; then
# Let USBGuard match policy rules: saved rules can omit the device's port.
# --show-devices indents the devices matching each policy rule.
policy=$(usbguard list-rules --show-devices) || approval_failed
policy=$(usbguard list-rules --label omarchy-usb-authorization-v1 --show-devices) || approval_failed
policy_allows=0
matched=0
while IFS= read -r line; do
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="org.omarchy.usb.approve">
<description>Trust a connected USB accessory on any port</description>
<message>Authentication is required to trust this USB accessory</message>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/omarchy-usb-authorization-approve</annotate>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
@@ -8,7 +8,6 @@ ConditionEnvironment=WAYLAND_DISPLAY
[Service]
Type=simple
ExecCondition=/usr/bin/systemctl is-active --quiet usbguard.service
ExecStart=/usr/bin/bash -lc 'exec "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch"'
Restart=always
RestartSec=2
@@ -0,0 +1,6 @@
polkit.addRule(function(action, subject) {
if (action.id === "org.omarchy.usb.approve" &&
subject.active && subject.local && subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});
+2
View File
@@ -1,5 +1,7 @@
echo "Configure USB device authorization"
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
# A deferred install has only the builder's peripherals attached. The owner
# needs working input before there is an account or a session for approvals.
if [[ -z ${OMARCHY_INSTALL_USER:-} ]]; then
@@ -0,0 +1,95 @@
# Device identity is portable; the full live rule is still used when granting
# access to the connection shown in the approval dialog.
usb_authorization_portable_rule() {
local rule="$1" token="" char quoted=0 escaped=0 index
local -a tokens=() portable=()
# Split canonical USBGuard output without evaluating device-controlled text.
# An attribute name inside a quoted name/serial is never a rule attribute.
for (( index=0; index<${#rule}; index++ )); do
char=${rule:index:1}
if (( escaped )); then
token+="$char"
escaped=0
elif (( quoted )) && [[ $char == '\' ]]; then
token+="$char"
escaped=1
elif [[ $char == '"' ]]; then
token+="$char"
quoted=$((1 - quoted))
elif (( ! quoted )) && [[ $char == " " ]]; then
if [[ -n $token ]]; then tokens+=("$token"); token=""; fi
else
token+="$char"
fi
done
(( ! quoted && ! escaped )) || return 1
if [[ -n $token ]]; then tokens+=("$token"); fi
[[ ${tokens[0]:-} == "allow" || ${tokens[0]:-} == "block" ]] || return 1
portable=(allow)
for (( index=1; index<${#tokens[@]}; index++ )); do
case "${tokens[index]}" in
via-port | parent-hash)
(( ++index < ${#tokens[@]} )) || return 1
[[ ${tokens[index]} == \"*\" ]] || return 1
;;
label) return 1 ;;
*) portable+=("${tokens[index]}") ;;
esac
done
local IFS=" "
printf '%s label "omarchy-usb-authorization-v1"\n' "${portable[*]}"
}
usb_authorization_permanent() {
local id="$1" expected="$2" devices current="" allowed portable policy line target new_id matches rule saved=0
[[ $id =~ ^[0-9]+$ && ( $expected == block\ * || $expected == allow\ * ) ]] || return 1
(( ${#expected} <= 16384 )) || return 1
devices=$(usbguard list-devices) || return 1
while IFS= read -r line; do
if [[ $line == "$id: "* ]]; then current=${line#"$id: "}; fi
done <<<"$devices"
[[ $current == "$expected" ]] || return 1
allowed="allow ${expected#* }"
portable=$(usb_authorization_portable_rule "$current") || return 1
usbguard-rule-parser "$portable" >/dev/null || return 1
# Respect earlier administrator deny rules. Do not report permanent success
# when a saved allow would lose to an explicit block/reject on reconnect.
policy=$(usbguard list-rules) || return 1
while IFS= read -r line; do
if [[ $line =~ ^[0-9]+:[[:space:]](allow|block|reject)([[:space:]]|$) ]]; then
target=${BASH_REMATCH[1]}
rule=${line#*: }
# --show-devices filters by current target, hiding deny rules for a
# temporarily allowed device. A match query compares identity instead.
matches=$(usbguard list-devices "match${rule#"$target"}") || return 1
if grep -Fqx -- "$id: $current" <<<"$matches"; then
if [[ $target != "allow" ]]; then
echo "An administrator rule blocks this device. Review that rule before saving approval." >&2
return 1
fi
break
fi
fi
done <<<"$policy"
while IFS= read -r line; do
if [[ $line =~ ^([0-9]+):[[:space:]] && ${line#*: } == "$portable" ]]; then
saved=1
fi
done <<<"$policy"
# A retry can reuse only an exact owned rule present in both the daemon and
# its persistent file. Never remove/reorder rules around manual restrictions.
if (( ! saved )) || ! grep -Fqx -- "$portable" /etc/usbguard/rules.conf; then
new_id=$(usbguard append-rule "$portable") || return 1
[[ $new_id =~ ^[0-9]+$ ]] || return 1
grep -Fqx -- "$portable" /etc/usbguard/rules.conf || return 1
fi
if [[ $current == block\ * ]]; then
usbguard allow-device "$expected" || return 1
fi
devices=$(usbguard list-devices --allowed) || return 1
grep -Fqx -- "$1: $allowed" <<<"$devices"
}
+20 -8
View File
@@ -1,3 +1,5 @@
source "${BASH_SOURCE[0]%/*}/usb-authorization-policy.sh"
usb_authorization_config_value() {
local config="$1" key="$2"
local awk_command=(awk)
@@ -54,15 +56,24 @@ usb_authorization_require_secure_settings() {
usb_authorization_require_secure_setting "$config" InsertedDevicePolicy apply-policy || return 1
usb_authorization_require_secure_setting "$config" AuthorizedDefault none || return 1
usb_authorization_require_secure_setting "$config" RestoreControllerDeviceState false || return 1
usb_authorization_require_secure_setting "$config" RuleFile /etc/usbguard/rules.conf || return 1
}
usb_authorization_generate_policy() {
local policy="$1"
local policy="$1" generated line
if ! usbguard generate-policy >"$policy"; then
if ! generated=$(usbguard generate-policy); then
echo "USBGuard could not enumerate devices; nothing was enabled." >&2
return 1
fi
: >"$policy"
while IFS= read -r line; do
if [[ $line == allow\ * ]]; then
usb_authorization_portable_rule "$line" >>"$policy" || return 1
elif [[ -n $line ]]; then
printf '%s\n' "$line" >>"$policy"
fi
done <<<"$generated"
if ! grep -q '^allow ' "$policy"; then
if grep -q '^[[:space:]]*[^#[:space:]]' "$policy"; then
echo "USBGuard did not generate a usable policy; nothing was enabled." >&2
@@ -72,6 +83,7 @@ usb_authorization_generate_policy() {
# Keep the file nonempty so re-enabling preserves this default-deny policy.
echo '# No USB devices were present during enrollment.' >>"$policy"
fi
usbguard-rule-parser -f "$policy" >/dev/null
}
usb_authorization_add_user() {
@@ -113,16 +125,16 @@ usb_authorization_provision_owner() {
}
usb_authorization_trust_present_devices() {
local line id
local line id devices
local count=0
local -a devices
devices=$(usbguard list-devices) || return 1
local -a inventory
mapfile -t inventory <<<"$devices"
mapfile -t devices < <(usbguard list-devices)
for line in "${devices[@]}"; do
for line in "${inventory[@]}"; do
if [[ $line =~ ^([0-9]+):[[:space:]] ]]; then
id="${BASH_REMATCH[1]}"
usbguard allow-device --permanent "$id"
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "${line#*: }" || return 1
(( ++count ))
fi
done
+4
View File
@@ -13,6 +13,10 @@ Omarchy takes security extremely seriously. This is meant to be an operating sys
USB device authorization is enabled by default. Omarchy uses USBGuard to trust the devices connected during installation and block USB devices it has not seen before. When a device is blocked, click the persistent notification and choose whether to allow it once, always allow it, or keep it blocked.
**Always allow this device** trusts the same device identity across USB ports and hubs, including after reboot. Changes to its identity still require approval. A hub may need its own approval before devices connected through it become available.
Older approvals can be tied to a port or hub. Run `omarchy-setup-security-usb-authorization --review-existing` to review connected devices and confirm which should be trusted on any port. Existing rules are preserved. For a disconnected device, reconnect it and choose **Always allow this device** if prompted.
On a preinstalled machine that asks you to create your account at first boot, USB protection starts at the end of that setup. It trusts the devices connected then, so your keyboard works throughout account setup even if it differs from the installer's keyboard.
You can turn the protection off from _Remove > Security > USB Device Authorization_. _Setup > Security > USB Device Authorization_ turns it back on with the existing trusted-device policy.
+7
View File
@@ -0,0 +1,7 @@
echo "Repair USB approval prompts and enable portable device approvals"
# Preserve an explicit opt-out. Existing users with an active/enabled daemon
# need a working watcher even if their old checkout link has become dangling.
if systemctl is-enabled --quiet usbguard.service || systemctl is-active --quiet usbguard.service; then
omarchy-setup-security-usb-authorization --yes
fi
@@ -0,0 +1,57 @@
#include <usbguard/Rule.hpp>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
static std::string read(const char* path) {
std::ifstream file(path);
std::string text;
std::getline(file, text);
if (text.empty()) throw std::runtime_error("empty rule");
return text;
}
static std::string replace(std::string text, const std::string& from, const std::string& to) {
const auto index = text.find(from);
if (index == std::string::npos) throw std::runtime_error("missing attribute");
text.replace(index, from.size(), to);
return text;
}
static void check(const char* name, const usbguard::Rule& policy, const usbguard::Rule& device, bool expected) {
if (policy.appliesTo(device) != expected) throw std::runtime_error(name);
std::cout << "ok - native USBGuard: " << name << '\n';
}
int main(int argc, char** argv) {
if (argc != 3) return 2;
// Test the policy produced by the real Bash implementation, not a C++ copy.
const auto policy = usbguard::Rule::fromString(read(argv[1]));
const auto text = read(argv[2]);
const auto device = usbguard::Rule::fromString(text);
check("original connection", policy, device, true);
auto moved = device;
moved.setViaPort("9-9.9");
check("another port", policy, moved, true);
moved.setParentHash("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
check("another hub", policy, moved, true);
auto reloaded = usbguard::Rule::fromString(policy.toString());
check("serialized policy reload on new hub", reloaded, moved, true);
check("reconnect on original port", reloaded, device, true);
auto different = moved;
different.setHash("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
check("different hash denied", policy, different, false);
different = moved;
different.setSerial("different");
check("different serial denied", policy, different, false);
different = moved;
different.setName("different");
check("different name denied", policy, different, false);
check("different product denied", policy, usbguard::Rule::fromString(replace(text, "046d:c53a", "046d:c53b")), false);
check("different vendor denied", policy, usbguard::Rule::fromString(replace(text, "046d:c53a", "046e:c53a")), false);
check("different interface denied", policy, usbguard::Rule::fromString(replace(text, "03:00:00", "08:06:50")), false);
check("different connection type denied", policy, usbguard::Rule::fromString(replace(text, "with-connect-type \"unknown\"", "with-connect-type \"hotplug\"")), false);
// Existing unmarked/manual rules remain location-bound.
check("legacy rule still restricts ports and hubs", device, moved, false);
}
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$ROOT/install/helpers/usb-authorization-policy.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
identity='id 046d:c53a serial "" name "USB Receiver" hash "KCUFt1MumW4Pfs/8YXWzGQB6Hsnm8qkDqFVjfY0NIBY="'
interfaces='with-interface { 03:01:01 03:01:02 03:00:00 } with-connect-type "unknown"'
location='parent-hash "m7yTNWlczwBbYn+uRP6TRsY5AceKmAZ0Et6mgy58+/o=" via-port "3-1.1.2.1"'
device="allow $identity $location $interfaces"
portable=$(usb_authorization_portable_rule "$device")
[[ $portable == "allow $identity $interfaces"' label "omarchy-usb-authorization-v1"' ]] || fail "only device location is removed"
[[ $(usb_authorization_portable_rule "${device/#allow/block}") == "$portable" ]] || fail "blocked approvals use the same portable identity"
pass "portable USB rules retain every device identity attribute"
# A canonical escaped name cannot cause its contents to become attributes.
name='name "Receiver \" via-port \"fake\" parent-hash \"fake\" $(touch /tmp/usb-injection)"'
hostile="allow id 046d:c53a serial \"\\\\\" $name hash \"hash\" $location $interfaces"
expected="allow id 046d:c53a serial \"\\\\\" $name hash \"hash\" $interfaces"' label "omarchy-usb-authorization-v1"'
[[ $(usb_authorization_portable_rule "$hostile") == "$expected" ]] || fail "quoted attribute text must remain data"
if usb_authorization_portable_rule 'allow id 046d:c53a name "unterminated' >/dev/null; then fail "reject unmatched quote"; fi
if usb_authorization_portable_rule 'allow id 046d:c53a via-port' >/dev/null; then fail "reject missing location value"; fi
pass "USB portable parsing preserves escaped and hostile descriptor text"
# Exercise the actual root entrypoint startup boundary without root work.
sed -e "s|source /usr/bin/omarchy-security-functions|source $ROOT/bin/omarchy-security-functions|" \
-e 's|source /usr/share/omarchy/install/helpers/usb-authorization-policy.sh|exit 126|' \
"$ROOT/bin/omarchy-usb-authorization-approve" >"$scratch/entry"
chmod 755 "$scratch/entry"
printf 'set -p\n' >"$scratch/decoy"
if BASH_ENV="$scratch/decoy" /usr/bin/bash "$scratch/entry" -p; then fail "ordinary Bash with decoy -p is rejected"; fi
printf 'touch "%s"\n' "$scratch/injected" >"$scratch/startup"
if BASH_ENV="$scratch/startup" "$scratch/entry" >/dev/null 2>&1; then fail "startup fixture boundary"; fi
if /usr/bin/env 'BASH_FUNC_source%%=() { touch "$USB_TEST_INJECTED"; }' USB_TEST_INJECTED="$scratch/injected" "$scratch/entry" >/dev/null 2>&1; then fail "exported function fixture boundary"; fi
[[ ! -e $scratch/injected ]] || fail "privileged entrypoint executes caller startup code"
pass "USB privileged approval rejects unsafe Bash startup"
if ! command -v g++ >/dev/null || [[ ! -f /usr/include/usbguard/Rule.hpp ]]; then
skip "native USBGuard matcher requires g++ and USBGuard development headers"
exit 0
fi
printf '%s\n' "$device" >"$scratch/device"
printf '%s\n' "$portable" >"$scratch/policy"
g++ -std=c++17 "$ROOT/test/shell.d/fixtures/usb-authorization/match.cpp" -lusbguard -o "$scratch/match"
"$scratch/match" "$scratch/policy" "$scratch/device"
+166 -18
View File
@@ -23,6 +23,7 @@ PresentDevicePolicy=apply-policy
InsertedDevicePolicy=apply-policy
AuthorizedDefault=none
RestoreControllerDeviceState=false
RuleFile=/etc/usbguard/rules.conf
CONF
cat >"$stub_bin/omarchy-pkg-missing" <<'STUB'
@@ -43,6 +44,7 @@ printf 'pkg-drop <%s>\n' "$*" >>"$CALLS"
STUB
cat >"$stub_bin/install" <<'STUB'
#!/bin/bash
if [[ ${!#} == /usr/share/polkit-1/actions/org.omarchy.usb.policy ]]; then exit 0; fi
if [[ $1 == -Dm600 && $2 == -o && $3 == root && $4 == -g && $5 == root ]]; then
exec /usr/bin/install -Dm600 "$6" "$7"
fi
@@ -53,6 +55,17 @@ cat >"$stub_bin/systemctl" <<'STUB'
printf 'systemctl <%s>\n' "$*" >>"$CALLS"
if [[ $* == "show usbguard.service --property=InvocationID --value" ]]; then
printf '%s\n' "${TEST_DAEMON_INVOCATION:-11111111111111111111111111111111}"
elif [[ $* == "--user show-environment" ]]; then
echo "OMARCHY_PATH=${TEST_SESSION_ROOT:-$OMARCHY_PATH}"
elif [[ $* == "--user restart omarchy-usb-authorization.service" ]]; then
[[ ${TEST_WATCHER_FAIL:-0} == 0 ]]
elif [[ $* == "restart usbguard.service" ]]; then
touch "$TEST_GUARD_ACTIVE"
elif [[ $* == "disable --now usbguard.service" ]]; then
[[ ${TEST_STOP_FAIL:-0} == 0 ]] || exit 1
[[ ${TEST_STOP_LIES:-0} == 1 ]] || rm -f "$TEST_GUARD_ACTIVE"
elif [[ $* == "is-active --quiet usbguard.service" ]]; then
[[ -f $TEST_GUARD_ACTIVE ]]
elif [[ $* == "is-enabled --quiet usbguard.service" ]]; then
exit 1
fi
@@ -100,11 +113,22 @@ list-devices)
echo '5: allow id 0627:0001 name "QEMU USB Tablet" hash "tablet"'
fi
;;
append-rule)
printf 'usbguard <append-rule> <%s>\n' "$2" >>"$CALLS"
[[ ${APPROVAL_EXIT_FAIL:-0} == 0 ]] || exit 1
if [[ ${APPROVAL_NO_POLICY:-0} == 0 ]]; then printf '%s\n' "$2" >"$TEST_SAVED_RULE"; fi
echo 40
;;
list-rules)
[[ ${APPROVAL_POLICY_QUERY_FAIL:-0} == 0 ]] || exit 1
if [[ -f $TEST_ALLOWED_RULE && ${APPROVAL_POLICY_QUERY_FAIL:-0} == 1 ]]; then exit 1; fi
if [[ -n ${TEST_POLICY_LIST:-} ]]; then cat "$TEST_POLICY_LIST"; fi
if [[ -f $TEST_SAVED_RULE ]]; then
printf '40: %s\n' "$(<"$TEST_SAVED_RULE")"
printf '\t17: %s\n' "$(<"$TEST_ALLOWED_RULE")"
if [[ -f $TEST_ALLOWED_RULE ]]; then
printf '\t17: %s\n' "$(<"$TEST_ALLOWED_RULE")"
elif [[ -n ${BLOCKED_DEVICE_RULE:-} ]]; then
printf '\t17: %s\n' "$BLOCKED_DEVICE_RULE"
fi
fi
;;
*)
@@ -140,6 +164,11 @@ elif [[ $1 == test && $2 == -s && $3 == /etc/usbguard/rules.conf ]]; then
exec /usr/bin/test -s "$TEST_RULES"
elif [[ $1 == install && $2 == -Dm600 && $3 == -o && $4 == root && $5 == -g && $6 == root && $8 == /etc/usbguard/rules.conf ]]; then
exec /usr/bin/install -Dm600 "$7" "$TEST_RULES"
elif [[ $1 == install && ${!#} == /usr/share/polkit-1/actions/org.omarchy.usb.policy ]]; then
exit 0
elif [[ $1 == /usr/bin/omarchy-usb-authorization-approve ]]; then
shift
exec "$USB_APPROVE_FIXTURE" "$@"
elif [[ $1 == omarchy-usb-authorization-restore-default ]]; then
echo 1 >"$TEST_SYSFS/usb1/authorized_default"
echo 1 >"$TEST_SYSFS/1-2/authorized"
@@ -165,7 +194,13 @@ choose)
fi
printf '%s\n' "${GUM_CHOICE:-Keep blocked}"
;;
confirm) exit 0 ;;
confirm)
if [[ ${GUM_REQUIRE_STDIN:-0} == 1 ]]; then
read -r consent
[[ $consent == yes ]] || exit 1
fi
[[ ${GUM_DECLINE:-0} == 0 ]]
;;
esac
STUB
cat >"$stub_bin/omarchy-notification-send" <<'STUB'
@@ -185,6 +220,16 @@ if [[ -n ${NOTIFICATION_READY_FILE:-} ]]; then
[[ -e $NOTIFICATION_READY_FILE ]]
fi
STUB
cat >"$stub_bin/pkexec" <<'STUB'
#!/bin/bash
[[ $1 == /usr/bin/omarchy-usb-authorization-approve ]] || exit 1
shift
exec "$USB_APPROVE_FIXTURE" "$@"
STUB
cat >"$stub_bin/usbguard-rule-parser" <<'STUB'
#!/bin/bash
exit 0
STUB
chmod +x "$stub_bin"/*
export HOME="$home"
@@ -196,6 +241,26 @@ export TEST_DAEMON_CONFIG="$daemon_config"
export TEST_RULES="$rules"
export TEST_ALLOWED_RULE="$scratch/allowed-rule"
export TEST_SAVED_RULE="$scratch/saved-rule"
export TEST_GUARD_ACTIVE="$scratch/guard-active"
export USB_APPROVE_FIXTURE="$scratch/approve"
export USB_POLICY_FIXTURE="$scratch/policy.sh"
sed "s|/etc/usbguard/rules.conf|$TEST_SAVED_RULE|g" "$ROOT/install/helpers/usb-authorization-policy.sh" >"$USB_POLICY_FIXTURE"
cat >"$USB_APPROVE_FIXTURE" <<'STUB'
#!/bin/bash
set -euo pipefail
source "$USB_POLICY_FIXTURE"
# Boot inventory uses the same helper; permanent behavior is exercised below.
if [[ $1 == 4 || $1 == 5 ]]; then
printf 'boot-approve <%s> <%s>\n' "$1" "$2" >>"$CALLS"
exit 0
fi
usb_authorization_permanent "$@"
STUB
chmod +x "$USB_APPROVE_FIXTURE"
sed -e "s|/usr/bin/omarchy-usb-authorization-approve|$USB_APPROVE_FIXTURE|g" \
-e "s|/usr/share/omarchy/install/helpers/usb-authorization-policy.sh|$USB_POLICY_FIXTURE|g" \
"$ROOT/bin/omarchy-setup-security-usb-authorization" >"$scratch/setup"
chmod +x "$scratch/setup"
if "$ROOT/bin/omarchy-usb-authorization-restore-default" >/dev/null 2>&1; then
fail "the fixed-path USB authorization restore helper requires root"
@@ -210,46 +275,76 @@ fi
fail "user-facing USB authorization commands do not elevate caller-selected paths"
pass "USB authorization keeps privileged paths fixed"
"$ROOT/bin/omarchy-setup-security-usb-authorization" --yes >"$scratch/setup-output"
"$scratch/setup" --yes >"$scratch/setup-output"
grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"' "$rules" ||
grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root" label "omarchy-usb-authorization-v1"' "$rules" ||
fail "USB authorization setup trusts devices present during enrollment"
[[ $(stat -c %a "$rules") == 600 ]] || fail "USB authorization policy is root-private"
grep -Fqx 'usbguard <add-user> <tester> <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" ||
fail "USB authorization grants only the IPC access needed by the approval flow"
grep -Fqx 'systemctl <restart usbguard.service>' "$calls" ||
fail "USB authorization reloads its user ACL before the watcher starts"
[[ -L $home/.config/systemd/user/omarchy-usb-authorization.service ]] ||
[[ -f $home/.config/systemd/user/omarchy-usb-authorization.service && ! -L $home/.config/systemd/user/omarchy-usb-authorization.service ]] ||
fail "USB authorization installs the graphical-session watcher"
grep -Fqx 'pkg-add <usbguard>' "$calls" || fail "USB authorization ensures USBGuard is installed"
pass "USB authorization enrolls present devices before enabling default-deny"
generate_count=$(grep -c '^usbguard <generate-policy>$' "$calls")
"$ROOT/bin/omarchy-setup-security-usb-authorization" --yes >"$scratch/setup-existing-output"
"$scratch/setup" --yes >"$scratch/setup-existing-output"
generate_count_after=$(grep -c '^usbguard <generate-policy>$' "$calls")
(( generate_count_after == generate_count )) || fail "re-enabling preserves the existing trusted-device policy"
pass "USB authorization setup preserves an existing policy"
# Failures before the watcher is ready must never start enforcement. A copied
# unit also repairs the dangling checkout link reported on the affected host.
unit="$home/.config/systemd/user/omarchy-usb-authorization.service"
rm -f "$unit"
ln -s "$scratch/deleted-checkout/service" "$unit"
"$scratch/setup" --yes >/dev/null
[[ -f $unit && ! -L $unit ]] || fail "setup repairs a dangling watcher unit"
for failure in missing-unit missing-helper session-mismatch watcher-start; do
rm -f "$TEST_GUARD_ACTIVE"
: >"$calls"
cp "$scratch/setup" "$scratch/failing-setup"
options=()
case "$failure" in
missing-unit) sed -i "s|^unit_source=.*|unit_source=$scratch/missing-unit|" "$scratch/failing-setup" ;;
missing-helper) sed -i "s|$USB_POLICY_FIXTURE|$scratch/missing-helper|g" "$scratch/failing-setup" ;;
session-mismatch) options+=(TEST_SESSION_ROOT=/missing-runtime) ;;
watcher-start) options+=(TEST_WATCHER_FAIL=1) ;;
esac
if env "${options[@]}" "$scratch/failing-setup" --yes >"$scratch/$failure.log" 2>&1; then
fail "setup must reject $failure"
fi
[[ ! -e $TEST_GUARD_ACTIVE ]] || fail "$failure starts blocking without a watcher"
! grep -Eq 'systemctl <(enable|restart) usbguard.service>' "$calls" || fail "$failure enables enforcement"
done
"$scratch/setup" --yes >/dev/null
watch_line=$(grep -n '^systemctl <--user restart omarchy-usb-authorization.service>' "$calls" | tail -1 | cut -d: -f1)
guard_line=$(grep -n '^systemctl <restart usbguard.service>' "$calls" | tail -1 | cut -d: -f1)
(( watch_line < guard_line )) || fail "watcher must start before enforcement"
pass "USB setup repairs dangling units and refuses enforcement without a working watcher"
: >"$rules"
GENERATE_EMPTY=1 "$ROOT/bin/omarchy-setup-security-usb-authorization" --yes >"$scratch/setup-empty-output"
GENERATE_EMPTY=1 "$scratch/setup" --yes >"$scratch/setup-empty-output"
[[ -s $rules ]] || fail "empty USB inventory must persist an initialized policy"
! grep -q '^allow ' "$rules" || fail "empty inventory must not add permissive rules"
generate_count=$(grep -c '^usbguard <generate-policy>$' "$calls")
"$ROOT/bin/omarchy-setup-security-usb-authorization" --yes >"$scratch/setup-empty-again-output"
"$scratch/setup" --yes >"$scratch/setup-empty-again-output"
[[ $(grep -c '^usbguard <generate-policy>$' "$calls") == "$generate_count" ]] ||
fail "re-enabling an empty policy must not enroll newly attached devices"
: >"$rules"
if GENERATE_FAIL=1 "$ROOT/bin/omarchy-setup-security-usb-authorization" --yes >"$scratch/setup-failed-output" 2>&1; then
if GENERATE_FAIL=1 "$scratch/setup" --yes >"$scratch/setup-failed-output" 2>&1; then
fail "enumeration failure must not be accepted as an empty USB inventory"
fi
[[ ! -s $rules ]] || fail "failed enumeration must not install a policy"
pass "USB setup accepts an empty inventory while rejecting enumeration failure"
: >"$calls"
"$ROOT/bin/omarchy-setup-security-usb-authorization" --boot --yes >"$scratch/setup-boot-output"
grep -Fqx 'usbguard <allow-device> <--permanent> <4>' "$calls" ||
"$scratch/setup" --boot --yes >"$scratch/setup-boot-output"
grep -Fq 'boot-approve <4>' "$calls" ||
fail "boot authorization trusts the connected root controller"
grep -Fqx 'usbguard <allow-device> <--permanent> <5>' "$calls" ||
grep -Fq 'boot-approve <5>' "$calls" ||
fail "boot authorization trusts the connected USB devices"
grep -Fqx 'sudo <omarchy-usb-authorization-boot> <enable>' "$calls" ||
fail "boot authorization uses the fixed-path boot-image helper"
@@ -419,7 +514,7 @@ export OMARCHY_INSTALL_USER=tester
export OMARCHY_USB_AUTHORIZATION_RULES_FILE="$rules"
export OMARCHY_USB_AUTHORIZATION_DAEMON_CONFIG="$daemon_config"
bash -euo pipefail -c 'source "$OMARCHY_INSTALL/config/usb-authorization.sh"' >"$scratch/install-output"
grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"' "$rules" ||
grep -qx 'allow id 1d6b:0002 name "Linux Foundation root hub" hash "root" label "omarchy-usb-authorization-v1"' "$rules" ||
fail "fresh installation enrolls devices present during installation"
grep -Fqx 'usbguard <add-user> <tester> <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" ||
fail "fresh installation grants the owner narrowly scoped approval access"
@@ -452,7 +547,7 @@ echo 'allow id 1234:0001 name "Builder keyboard" hash "builder"' >"$rules"
owner_rule='allow id 1234:0002 name "Owner keyboard" hash "owner"'
: >"$calls"
GENERATED_RULE="$owner_rule" usb_authorization_provision_owner owner "$rules" "$daemon_config"
[[ $(<"$rules") == "$owner_rule" ]] || fail "owner enrollment must replace inherited builder trust"
[[ $(<"$rules") == "$owner_rule"' label "omarchy-usb-authorization-v1"' ]] || fail "owner enrollment must replace inherited builder trust"
grep -Fqx 'usbguard <add-user> <owner> <--devices=list,listen,modify> <--policy=list> <--exceptions=listen>' "$calls" ||
fail "owner provisioning grants approval access"
[[ $(tail -2 "$calls") == $'systemctl <enable usbguard.service>\nsystemctl <restart usbguard.service>' ]] ||
@@ -462,7 +557,7 @@ grep -Fqx 'usbguard <add-user> <owner> <--devices=list,listen,modify> <--policy=
if GENERATE_FAIL=1 usb_authorization_provision_owner owner "$rules" "$daemon_config" >"$scratch/owner-failed" 2>&1; then
fail "owner enrollment must reject enumeration failure"
fi
[[ $(<"$rules") == "$owner_rule" ]] || fail "failed enrollment preserves the old policy"
[[ $(<"$rules") == "$owner_rule"' label "omarchy-usb-authorization-v1"' ]] || fail "failed enrollment preserves the old policy"
! grep -q '^systemctl' "$calls" || fail "failed owner enrollment must not enable enforcement"
: >"$calls"
sed 's/ImplicitPolicyTarget=block/ImplicitPolicyTarget=allow/' "$daemon_config" >"$scratch/insecure-daemon.conf"
@@ -622,10 +717,37 @@ USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
request=$(find "$home/.local/state/omarchy/usb-authorization/requests" -maxdepth 1 -name 'request-*.json' -print -quit)
token=$(basename "${request%.json}")
GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-always-output"
grep -Fqx "usbguard <allow-device> <--permanent> <$malicious_rule>" "$calls" || fail "review can persist an exact-device rule"
grep -Fq 'usbguard <append-rule> <allow ' "$calls" || fail "review can persist a portable device rule"
append_count=$(grep -c '^usbguard <append-rule>' "$calls")
"$USB_APPROVE_FIXTURE" 17 "allow ${malicious_rule#block }"
[[ $(grep -c '^usbguard <append-rule>' "$calls") == "$append_count" ]] || fail "confirmed permanent retry must preserve rule ordering without duplicate rules"
pass "USB review supports explicit persistent trust"
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
# Unmarked rules might be intentional manual restrictions. Review is opt-in,
# never accepted through --yes, and only the selected live identity is added.
legacy_rule='allow id 046d:c53a name "Receiver" hash "receiver" parent-hash "hub-a" via-port "1-2"'
printf '%s\n' "$legacy_rule" >"$TEST_ALLOWED_RULE"
printf '12: %s\n\t17: %s\n' "$legacy_rule" "$legacy_rule" >"$scratch/legacy-policy"
cp "$scratch/legacy-policy" "$scratch/legacy-before"
if "$scratch/setup" --review-existing --yes >/dev/null 2>&1; then fail "legacy migration requires individual consent"; fi
TEST_POLICY_LIST="$scratch/legacy-policy" GUM_DECLINE=1 "$scratch/setup" --review-existing >/dev/null
[[ ! -e $TEST_SAVED_RULE ]] || fail "declining migration adds portable trust"
printf 'yes\nyes\nyes\n' | TEST_POLICY_LIST="$scratch/legacy-policy" GUM_REQUIRE_STDIN=1 "$scratch/setup" --review-existing >/dev/null
[[ $(<"$TEST_SAVED_RULE") == 'allow id 046d:c53a name "Receiver" hash "receiver" label "omarchy-usb-authorization-v1"' ]] || fail "legacy review saves only identity constraints"
cmp -s "$scratch/legacy-policy" "$scratch/legacy-before" || fail "legacy review rewrites existing manual rules"
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
printf '12: block id 07a6:8513\n\t17: %s\n' "$malicious_rule" >"$scratch/deny-policy"
if TEST_POLICY_LIST="$scratch/deny-policy" "$USB_APPROVE_FIXTURE" 17 "$malicious_rule" >/dev/null 2>&1; then fail "permanent approval must respect earlier manual denial"; fi
[[ ! -e $TEST_SAVED_RULE && ! -e $TEST_ALLOWED_RULE ]] || fail "manual denial changed policy or authorization"
printf '%s\n' "allow ${malicious_rule#block }" >"$TEST_ALLOWED_RULE"
if TEST_POLICY_LIST="$scratch/deny-policy" "$USB_APPROVE_FIXTURE" 17 "allow ${malicious_rule#block }" >/dev/null 2>&1; then fail "temporary allow cannot hide an earlier manual denial"; fi
rm -f "$TEST_ALLOWED_RULE"
printf '12: block\n' >"$scratch/deny-all-policy"
if TEST_POLICY_LIST="$scratch/deny-all-policy" "$USB_APPROVE_FIXTURE" 17 "$malicious_rule" >/dev/null 2>&1; then fail "unconditional manual denial must be preserved"; fi
pass "USB legacy review requires consent and preserves manual policy"
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID=17 \
USBGUARD_DEVICE_EVENT=Insert \
@@ -724,11 +846,12 @@ if APPROVAL_NO_POLICY=1 GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy
fail "permanent approval requires saved policy"
fi
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
if GUM_CHOICE='Allow once' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/missing-policy-retry" 2>&1; then
if APPROVAL_NO_POLICY=1 GUM_CHOICE='Allow once' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/missing-policy-retry" 2>&1; then
fail "retry cannot downgrade permanent approval when policy is missing"
fi
[[ -f $request && $(jq -r .approval "$request") == 'Always allow this device' ]] || fail "missing policy retains original permanent intent"
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "retry must not repeat authorization on an allowed device"
printf '%s\n' "allow ${malicious_rule#block }" >"$TEST_ALLOWED_RULE"
for failure in BLOCKED_QUERY_FAIL APPROVAL_QUERY_FAIL; do
if env "$failure=1" "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/query-retry" 2>&1; then
fail "retry must fail when inventory cannot be read"
@@ -803,6 +926,18 @@ echo 0 >"$sysfs/usb1/authorized_default"
echo 0 >"$sysfs/1-2/authorized"
export TEST_SYSFS="$sysfs"
for failure in TEST_STOP_FAIL TEST_STOP_LIES; do
touch "$TEST_GUARD_ACTIVE"
: >"$calls"
if env "$failure=1" "$ROOT/bin/omarchy-remove-security-usb-authorization" --yes >"$scratch/$failure.log" 2>&1; then
fail "removal must reject $failure"
fi
[[ -f $unit ]] || fail "failed daemon stop removes watcher"
! grep -q '^usbguard <remove-user>' "$calls" || fail "failed daemon stop revokes watcher access"
! grep -q '^systemctl <--user disable' "$calls" || fail "failed daemon stop disables watcher"
done
pass "USB removal preserves approval prompts if stopping enforcement fails"
"$ROOT/bin/omarchy-remove-security-usb-authorization" --yes >"$scratch/remove-output"
[[ $(<"$sysfs/usb1/authorized_default") == 1 ]] || fail "removal restores root-hub default authorization"
@@ -817,3 +952,16 @@ grep -Fqx 'sudo <omarchy-usb-authorization-boot> <disable>' "$calls" ||
[[ ! -e $home/.config/systemd/user/omarchy-usb-authorization.service ]] ||
fail "removal deletes the graphical-session watcher"
pass "USB authorization removal restores the original default-allow behavior"
cat >"$stub_bin/omarchy-setup-security-usb-authorization" <<'STUB'
#!/bin/bash
printf 'migrate-setup <%s>\n' "$*" >>"$CALLS"
STUB
chmod +x "$stub_bin/omarchy-setup-security-usb-authorization"
: >"$calls"
bash -euo pipefail "$ROOT/migrations/1790608617.sh" >/dev/null
! grep -q '^migrate-setup' "$calls" || fail "migration overrides USB opt-out"
touch "$TEST_GUARD_ACTIVE"
bash -euo pipefail "$ROOT/migrations/1790608617.sh" >/dev/null
grep -Fqx 'migrate-setup <--yes>' "$calls" || fail "migration repairs an active daemon without a watcher"
pass "USB migration repairs enabled protection and preserves opt-out"