Skip to content

Commit 90d604f

Browse files
committed
Expire USB approval deduplication across daemon and watcher restarts
1 parent afad853 commit 90d604f

3 files changed

Lines changed: 125 additions & 16 deletions

File tree

‎bin/omarchy-usb-authorization-event‎

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,43 @@
66
set -euo pipefail
77

88
case "${USBGUARD_IPC_SIGNAL:-}" in
9-
IPC.Connected)
9+
IPC.Connected) ;;
10+
Device.PolicyApplied)
11+
# PresenceChanged precedes policy evaluation, even for trusted devices.
12+
[[ ${USBGUARD_DEVICE_TARGET_NEW:-} == "block" ]] || exit 0
13+
;;
14+
Device.PresenceChanged)
15+
[[ ${USBGUARD_DEVICE_EVENT:-} == "Remove" ]] || exit 0
16+
;;
17+
*) exit 0 ;;
18+
esac
19+
20+
requests="$HOME/.local/state/omarchy/usb-authorization/requests"
21+
mkdir -p "$requests"
22+
chmod 700 "${requests%/*}" "$requests"
23+
exec 9>"$requests/.lock"
24+
25+
request_generation() {
26+
local daemon_id
27+
28+
# USBGuard reuses device IDs after restarting. Include both the daemon
29+
# invocation and watcher lifetime so old prompts cannot suppress new ones.
30+
daemon_id=$(systemctl show usbguard.service --property=InvocationID --value) || return 1
31+
[[ $daemon_id =~ ^[0-9a-f]{32}$ ]] || return 1
32+
printf '%s:%s' "${OMARCHY_USB_AUTHORIZATION_WATCH_ID:-standalone}" "$daemon_id"
33+
}
34+
35+
if [[ $USBGUARD_IPC_SIGNAL == "IPC.Connected" ]]; then
36+
flock 9
37+
generation=$(request_generation) || exit 1
38+
for pending in "$requests"/request-*.json; do
39+
[[ -f $pending ]] || continue
40+
if ! jq -e --arg generation "$generation" '.generation == $generation' "$pending" >/dev/null 2>&1; then
41+
rm -f "$pending"
42+
fi
43+
done
44+
flock -u 9
45+
1046
# USBGuard scans present devices before accepting IPC connections. Repeat
1147
# this on every reconnect: temporary approvals disappear on daemon restart.
1248
blocked=$(usbguard list-devices --blocked) || exit 1
@@ -20,21 +56,14 @@ IPC.Connected)
2056
fi
2157
done <<<"$blocked"
2258
exit 0
23-
;;
24-
Device.PolicyApplied)
25-
# PresenceChanged precedes policy evaluation, even for trusted devices.
26-
# PolicyApplied also covers block-to-block and allow-to-block transitions.
27-
[[ ${USBGUARD_DEVICE_TARGET_NEW:-} == "block" ]] || exit 0
28-
;;
29-
*) exit 0 ;;
30-
esac
59+
fi
3160

3261
[[ ${USBGUARD_DEVICE_ID:-} =~ ^[0-9]+$ ]] || exit 0
3362
[[ -n ${USBGUARD_DEVICE_RULE:-} ]] || exit 0
3463

35-
requests="$HOME/.local/state/omarchy/usb-authorization/requests"
36-
mkdir -p "$requests"
37-
chmod 700 "${requests%/*}" "$requests"
64+
# A removed device's final target may be allow even though a blocked request
65+
# remains. Its identity attributes are unchanged; normalize the target.
66+
USBGUARD_DEVICE_RULE="block ${USBGUARD_DEVICE_RULE#* }"
3867

3968
# The token is safe to carry through the presentation launcher's command
4069
# string. The device-controlled rule itself stays in a mode-0600 JSON file and
@@ -43,12 +72,24 @@ token="request-$(printf '%s\0%s' "$USBGUARD_DEVICE_ID" "$USBGUARD_DEVICE_RULE" |
4372
request="$requests/$token.json"
4473
# watch executes handlers concurrently. A reconnect scan and a policy event
4574
# can refer to the same device; publish only one pending request.
46-
exec 9>"$requests/.lock"
75+
if [[ $USBGUARD_IPC_SIGNAL == "Device.PresenceChanged" ]]; then
76+
flock 9
77+
blocked=$(usbguard list-devices --blocked) || exit 1
78+
# A delayed removal event must not erase a request for a reappeared device.
79+
if ! grep -Fqx -- "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" <<<"$blocked"; then
80+
rm -f "$request"
81+
fi
82+
exit 0
83+
fi
84+
4785
# Keep retrying without needing another USB event. Handlers belong to the
4886
# watcher service and are stopped with the graphical session.
4987
while true; do
5088
flock 9
51-
[[ ! -e $request ]] || exit 0
89+
generation=$(request_generation) || exit 1
90+
if [[ -f $request ]] && jq -e --arg generation "$generation" '.generation == $generation' "$request" >/dev/null 2>&1; then
91+
exit 0
92+
fi
5293

5394
# Discard events overtaken by another policy decision or device replacement.
5495
blocked=$(usbguard list-devices --blocked) || exit 1
@@ -75,7 +116,8 @@ while true; do
75116
jq -n \
76117
--arg id "$USBGUARD_DEVICE_ID" \
77118
--arg rule "$USBGUARD_DEVICE_RULE" \
78-
'{id: $id, rule: $rule}' >"$temporary"
119+
--arg generation "$generation" \
120+
'{id: $id, rule: $rule, generation: $generation}' >"$temporary"
79121
chmod 600 "$temporary"
80122
mv -f "$temporary" "$request"
81123

‎bin/omarchy-usb-authorization-watch‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
set -euo pipefail
77

8+
# Starting a new graphical-session watcher must offer fresh prompts even if
9+
# their old notification actions were dismissed and aged out of history.
10+
OMARCHY_USB_AUTHORIZATION_WATCH_ID=$(cat /proc/sys/kernel/random/uuid)
11+
export OMARCHY_USB_AUTHORIZATION_WATCH_ID
12+
813
event_handler="$OMARCHY_PATH/bin/omarchy-usb-authorization-event"
914

1015
# The handler scans blocked devices on IPC.Connected, including reconnects

‎test/shell.d/usb-authorization-test.sh‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ STUB
5151
cat >"$stub_bin/systemctl" <<'STUB'
5252
#!/bin/bash
5353
printf 'systemctl <%s>\n' "$*" >>"$CALLS"
54-
if [[ $* == "is-enabled --quiet usbguard.service" ]]; then
54+
if [[ $* == "show usbguard.service --property=InvocationID --value" ]]; then
55+
printf '%s\n' "${TEST_DAEMON_INVOCATION:-11111111111111111111111111111111}"
56+
elif [[ $* == "is-enabled --quiet usbguard.service" ]]; then
5557
exit 1
5658
fi
5759
STUB
@@ -97,6 +99,9 @@ list-rules)
9799
printf 'usbguard' >>"$CALLS"
98100
printf ' <%s>' "$@" >>"$CALLS"
99101
printf '\n' >>"$CALLS"
102+
if [[ $1 == "watch" && -n ${TEST_WATCH_ID_FILE:-} ]]; then
103+
printf '%s\n' "$OMARCHY_USB_AUTHORIZATION_WATCH_ID" >"$TEST_WATCH_ID_FILE"
104+
fi
100105
if [[ $1 == "allow-device" && ${!#} == block* ]]; then
101106
[[ ${APPROVAL_EXIT_FAIL:-0} == 0 ]] || exit 1
102107
if [[ ${APPROVAL_SILENT_FAIL:-0} == 0 ]]; then
@@ -506,6 +511,63 @@ wait "$scan_pid"
506511
fail "overlapping scan and policy events must not duplicate pending prompts"
507512
pass "USB reconnect scans and policy events deduplicate pending requests"
508513

514+
# Dismissing/canceling a notification leaves its request, even after the shell
515+
# discards the archived action. Reusing the ID after daemon restart must prompt.
516+
notification_count=$(grep -c '^notification' "$calls")
517+
export TEST_DAEMON_INVOCATION=22222222222222222222222222222222
518+
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event" &
519+
scan_pid=$!
520+
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
521+
USBGUARD_DEVICE_ID=17 \
522+
USBGUARD_DEVICE_TARGET_NEW=block \
523+
USBGUARD_DEVICE_RULE="$malicious_rule" \
524+
"$ROOT/bin/omarchy-usb-authorization-event"
525+
wait "$scan_pid"
526+
[[ $(grep -c '^notification' "$calls") == $((notification_count + 1)) ]] ||
527+
fail "reused IDs must get exactly one new notification after daemon restart"
528+
[[ -f $request ]] || fail "the recovered notification must have a reviewable request"
529+
530+
# Starting another watcher/session also expires old notification deduplication.
531+
notification_count=$(grep -c '^notification' "$calls")
532+
export OMARCHY_USB_AUTHORIZATION_WATCH_ID=new-session
533+
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
534+
[[ $(grep -c '^notification' "$calls") == $((notification_count + 1)) ]] ||
535+
fail "a new watcher must recover an orphaned approval action"
536+
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
537+
[[ $(grep -c '^notification' "$calls") == $((notification_count + 1)) ]] ||
538+
fail "same-generation scans must still deduplicate"
539+
540+
# A removal that arrives late must not erase a new connection's request.
541+
USBGUARD_IPC_SIGNAL=Device.PresenceChanged USBGUARD_DEVICE_EVENT=Remove \
542+
USBGUARD_DEVICE_ID=17 USBGUARD_DEVICE_RULE="$malicious_rule" \
543+
"$ROOT/bin/omarchy-usb-authorization-event"
544+
[[ -f $request ]] || fail "delayed removal must preserve a currently blocked device's request"
545+
BLOCKED_DEVICE_PRESENT=0 \
546+
USBGUARD_IPC_SIGNAL=Device.PresenceChanged USBGUARD_DEVICE_EVENT=Remove \
547+
USBGUARD_DEVICE_ID=17 USBGUARD_DEVICE_RULE="${malicious_rule/#block/allow}" \
548+
"$ROOT/bin/omarchy-usb-authorization-event"
549+
[[ ! -e $request ]] || fail "device removal must clean up its pending request"
550+
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
551+
[[ $(grep -c '^notification' "$calls") == $((notification_count + 2)) ]] ||
552+
fail "a reappeared device must receive a fresh notification"
553+
554+
# Upgrade from requests written before generations existed, and remove stale
555+
# requests for devices no longer in the inventory.
556+
jq 'del(.generation)' "$request" >"$scratch/legacy-request"
557+
mv "$scratch/legacy-request" "$request"
558+
legacy_orphan="${request%/*}/request-$(printf '%064d' 0).json"
559+
printf '{"id":"99","rule":"block id 0000:0000"}\n' >"$legacy_orphan"
560+
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
561+
[[ $(grep -c '^notification' "$calls") == $((notification_count + 3)) ]] ||
562+
fail "legacy request markers must not suppress notification recovery"
563+
[[ ! -e $legacy_orphan ]] || fail "reconnect must clean up old-generation orphan requests"
564+
565+
TEST_WATCH_ID_FILE="$scratch/watch-one" "$ROOT/bin/omarchy-usb-authorization-watch"
566+
TEST_WATCH_ID_FILE="$scratch/watch-two" "$ROOT/bin/omarchy-usb-authorization-watch"
567+
[[ -s $scratch/watch-one && $(<"$scratch/watch-one") != "$(<"$scratch/watch-two")" ]] ||
568+
fail "each watcher must export a fresh lifetime identifier"
569+
pass "USB prompts recover across daemon and session generations and device removal"
570+
509571
GUM_CHOICE='Allow once' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-once-output"
510572
grep -Fqx "usbguard <allow-device> <$malicious_rule>" "$calls" || fail "review can allow the exact device once"
511573
[[ ! -e $request ]] || fail "a completed review consumes its request"

0 commit comments

Comments
 (0)