Re-check Bolt less often while there is no Thunderbolt controller (#14931)
Both Thunderbolt loops reconciled or rescanned every 2 s even on machines with no controller: on an idle M2 Max that was about 1,590 process launches a minute. While nothing is connected or boot-protected and no controller is present, the daemon now reconciles every 10 s, inside a snapshot's 20 s lifetime, and returns to every tick as soon as a controller, a policy change or a boot recovery checkpoint appears. The watcher skips an unchanged, fresh, device-free snapshot, and both loops wait without forking sleep.
This commit is contained in:
1 parent
77bfaca408
commit
6fee7105af
4 files changed
+150
-8
No files matched your search
@@ -14,7 +14,7 @@ The Bolt startup drop-in sets the persisted mode before hardware enumeration whe
|
||||
|
||||
Secure-capable devices require both a stored Bolt record and a saved key for automatic trust. An explicit permanent approval pins the current sysfs directory and key descriptor, seeds an empty kernel key before authorization, and verifies Bolt imported it before saving consent. Automatic reconciliation cannot initialize a missing key and retains its read-only sysfs sandbox. First activation is deferred when a captured secure-capable device lacks a saved Bolt key: the `.pending` marker starts a setup notification, while existing Bolt configuration and services remain untouched. Installation can finish; only explicit setup after safe disconnection clears the pending state. Initial identity capture uses Bolt's numeric device/vendor fallback when descriptive sysfs attributes are absent.
|
||||
|
||||
The runtime is Bash, with `busctl --json=short` for Bolt's structured D-Bus replies and `jq` for JSON. The root daemon publishes a root-owned snapshot in `/run/omarchy-thunderbolt-authorization`. The user service polls that snapshot and refuses unavailable, stale, or mismatched generations. It does not infer final policy from presence signals. Requests use opaque tokens and bind the policy-service generation, Bolt bus owner, device path, connection timestamp, sysfs inode, and device identity. The inode distinguishes same-second reconnects. Device text is displayed as untrusted data. Failed delivery retries; lost approval responses retain intent and offer another notification. Watcher restarts preserve unfinished intent only while the exact connection and policy-service incarnation remain unchanged. A changed device or daemon requires a new request.
|
||||
The runtime is Bash, with `busctl --json=short` for Bolt's structured D-Bus replies and `jq` for JSON. The root daemon publishes a root-owned snapshot in `/run/omarchy-thunderbolt-authorization`. The user service polls that snapshot and refuses unavailable, stale, or mismatched generations. Both loops tick every 2 s without forking. While Linux sees no Thunderbolt controller and nothing is connected or boot-protected, the daemon reconciles Bolt every 10 s instead of every tick, still well inside a snapshot's 20 s lifetime; a controller appearing, a policy change or a boot recovery checkpoint restores the 2 s cadence at once. The user service skips an unchanged, fresh snapshot with no devices, warnings or requests in flight. It does not infer final policy from presence signals. Requests use opaque tokens and bind the policy-service generation, Bolt bus owner, device path, connection timestamp, sysfs inode, and device identity. The inode distinguishes same-second reconnects. Device text is displayed as untrusted data. Failed delivery retries; lost approval responses retain intent and offer another notification. Watcher restarts preserve unfinished intent only while the exact connection and policy-service incarnation remain unchanged. A changed device or daemon requires a new request.
|
||||
|
||||
The terminal dialog saves an approval intent; the background watcher submits it through `pkexec` to the fixed installed `/usr/bin/omarchy-thunderbolt-authorization-approve` helper. Polkit grants active local wheel users this individual approval action. The helper validates its arguments and the live connection, then independently verifies authorization and any saved trust before returning success. Root entrypoints use protected Bash startup and fixed installed libraries; they discard caller-supplied D-Bus addresses. A shared lock serializes setup, reconciliation, and approvals. General setup uses a separate administrator command invoked with `sudo` from the terminal. Blanket Bolt management requires administrator authentication while protection is enabled. These controls protect against unsolicited accessories, not malicious software already running as an administrator. Identifiers can be forged; Bolt's secure connection keys provide additional verification on supported hardware.
|
||||
|
||||
|
||||
@@ -13,11 +13,29 @@ TB_DEVICE=org.freedesktop.bolt1.Device
|
||||
TB_DOMAIN=org.freedesktop.bolt1.Domain
|
||||
TB_PATH=/org/freedesktop/bolt
|
||||
TB_SYSFS=/sys/bus/thunderbolt/devices
|
||||
# How often the daemon re-checks Bolt while there is no controller and no boot
|
||||
# protection: well inside the 20 s a published snapshot stays valid.
|
||||
TB_IDLE_RECHECK=10
|
||||
TB_TRUST_JQ='def trusted($state): $state.trusted[.Uid] == {Uid,Name,Vendor} and
|
||||
((.Secure | not) or (.Stored and (.Key | IN("have","new"))));'
|
||||
|
||||
tb_fail() { printf '%s\n' "$*" >&2; return 1; }
|
||||
|
||||
# Sleeps without forking on each tick: a pipe open for reading and writing
|
||||
# never reaches EOF, so a timed read on it returns only on timeout or a signal.
|
||||
tb_wait() {
|
||||
[[ -n ${TB_TICK:-} ]] || exec {TB_TICK}<> <(:)
|
||||
read -r -t "$1" -u "$TB_TICK" _ || true
|
||||
}
|
||||
|
||||
tb_controller_present() {
|
||||
local domain
|
||||
for domain in "$TB_SYSFS"/domain*; do
|
||||
[[ -e $domain ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
tb_json_write() {
|
||||
local target=$1 mode=${2:-600} temporary
|
||||
temporary=$(mktemp "${target%/*}/.thunderbolt.XXXXXX") || return 1
|
||||
@@ -287,11 +305,24 @@ tb_reconcile() {
|
||||
}
|
||||
|
||||
tb_daemon() {
|
||||
local snapshot error
|
||||
local snapshot error policy idle_policy='' idle_since=0
|
||||
install -d -m 755 "$TB_RUNTIME"
|
||||
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
|
||||
chmod 644 "$TB_RUNTIME/generation"
|
||||
while [[ -f $TB_MARKER ]]; do
|
||||
policy=''
|
||||
[[ -r $TB_STATE ]] && IFS= read -r -d '' policy < "$TB_STATE" || true
|
||||
# With no controller, no device and no boot protection there is nothing to
|
||||
# restore or reapply, so Bolt is reconciled every TB_IDLE_RECHECK seconds
|
||||
# instead of every tick. A controller appearing, a policy change or a boot
|
||||
# recovery checkpoint brings back the full check at once.
|
||||
if [[ -n $idle_policy && $policy == "$idle_policy" && ! -e ${TB_STATE%/*}/boot-recovery.json ]] &&
|
||||
(( EPOCHSECONDS - idle_since >= 0 && EPOCHSECONDS - idle_since < TB_IDLE_RECHECK )) &&
|
||||
! tb_controller_present; then
|
||||
tb_wait 2
|
||||
continue
|
||||
fi
|
||||
idle_policy=''
|
||||
(
|
||||
flock -x 9
|
||||
if snapshot=$(tb_reconcile 2> "$TB_RUNTIME/error"); then
|
||||
@@ -303,7 +334,13 @@ tb_daemon() {
|
||||
tb_json_write "$TB_RUNTIME/snapshot.json" 644
|
||||
fi
|
||||
) 9> "$TB_LOCK"
|
||||
sleep 2
|
||||
snapshot=''
|
||||
[[ -r $TB_RUNTIME/snapshot.json ]] && IFS= read -r -d '' snapshot < "$TB_RUNTIME/snapshot.json" || true
|
||||
if [[ -n $policy ]] && jq -e '.available == true and .domains == [] and .devices == [] and
|
||||
.warnings == [] and .error == "" and .boot_protection == false' <<< "$snapshot" >/dev/null 2>&1; then
|
||||
idle_policy=$policy idle_since=$EPOCHSECONDS
|
||||
fi
|
||||
tb_wait 2
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -139,15 +139,28 @@ tb_scan() {
|
||||
}
|
||||
|
||||
tb_watch() {
|
||||
local watch snapshot warning warned='' failures=0
|
||||
local watch snapshot warning warned='' failures=0 text generation clean='' clean_generation='' clean_time=0
|
||||
tb_requests
|
||||
watch=$(cat /proc/sys/kernel/random/uuid)
|
||||
while true; do
|
||||
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
|
||||
if [[ $warned != "pending" ]] && tb_setup_notice; then warned=pending; fi
|
||||
sleep 2
|
||||
clean=''
|
||||
tb_wait 2
|
||||
continue
|
||||
fi
|
||||
# The last scan's snapshot, unchanged, still fresh and from the same daemon,
|
||||
# with no device, warning or request in flight, has nothing to deliver.
|
||||
text='' generation=''
|
||||
[[ -r $TB_RUNTIME/snapshot.json ]] && IFS= read -r -d '' text < "$TB_RUNTIME/snapshot.json" || true
|
||||
[[ -r $TB_RUNTIME/generation ]] && IFS= read -r generation < "$TB_RUNTIME/generation" || true
|
||||
if [[ -n $clean && ${text%$'\n'} == "$clean" && $generation == "$clean_generation" ]] &&
|
||||
(( EPOCHSECONDS - clean_time >= 0 && EPOCHSECONDS - clean_time < 20 )) &&
|
||||
! compgen -G "$TB_REQUESTS/request-*.json" >/dev/null; then
|
||||
tb_wait 2
|
||||
continue
|
||||
fi
|
||||
clean=''
|
||||
if snapshot=$(tb_scan "$watch"); then
|
||||
failures=0
|
||||
warning=$(jq -c '[.generation,.warnings,.error]' <<< "$snapshot")
|
||||
@@ -157,6 +170,10 @@ tb_watch() {
|
||||
fi
|
||||
else
|
||||
warned=''
|
||||
if jq -e '.devices == []' <<< "$snapshot" >/dev/null; then
|
||||
IFS=' ' read -r clean_generation clean_time <<< "$(jq -r '"\(.generation) \(.time)"' <<< "$snapshot")"
|
||||
[[ $clean_time =~ ^[0-9]+$ ]] && clean=$snapshot
|
||||
fi
|
||||
fi
|
||||
else
|
||||
failures=$((failures + 1))
|
||||
@@ -164,7 +181,7 @@ tb_watch() {
|
||||
warned=unavailable
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
tb_wait 2
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ test_deferred_opt_out_retains_initial_enrollment() {
|
||||
test_pending_notice_retries_without_claiming_protection() {
|
||||
touch "$TB_PENDING"
|
||||
local count=0 notices=0
|
||||
sleep() { count=$((count + 1)); (( count < 3 )) || exit 99; }
|
||||
tb_wait() { count=$((count + 1)); (( count < 3 )) || exit 99; }
|
||||
tb_setup_notice() { notices=$((notices + 1)); (( notices > 1 )) && echo pending >> "$T/notifications"; }
|
||||
reject tb_watch
|
||||
[[ $(cat "$T/notifications") == "pending" ]]
|
||||
@@ -567,7 +567,7 @@ test_trusted_reconnect_no_alert() {
|
||||
}
|
||||
test_unavailable_warns_once() {
|
||||
tb_scan() { return 1; }
|
||||
sleep() { count=$((count+1)); ((count<4)) || exit 99; }
|
||||
tb_wait() { count=$((count+1)); ((count<4)) || exit 99; }
|
||||
local count=0
|
||||
reject tb_watch
|
||||
[[ $(cat "$T/notifications") == "attention" ]]
|
||||
@@ -629,6 +629,94 @@ test_snapshot_freshness_and_disabled_state() {
|
||||
reject tb_public_snapshot
|
||||
echo generation-b > "$TB_RUNTIME/generation"; reject tb_public_snapshot
|
||||
}
|
||||
test_wait_times_out_without_input() {
|
||||
local start=$EPOCHSECONDS
|
||||
TB_TICK='' tb_wait 1
|
||||
(( EPOCHSECONDS - start >= 1 && EPOCHSECONDS - start < 10 ))
|
||||
}
|
||||
test_daemon_rechecks_controller_free_bolt_less_often() {
|
||||
touch "$TB_MARKER"; TB_IDLE_RECHECK=600
|
||||
local tick=0 extra='{}'
|
||||
expect() { [[ $(wc -l < "$T/reconciles") == "$1" ]] || fail "tick $tick: expected $1 reconciles" "$(wc -l < "$T/reconciles")"; }
|
||||
tb_reconcile() {
|
||||
echo >> "$T/reconciles"
|
||||
jq -cn --argjson time "$EPOCHSECONDS" --argjson extra "$extra" \
|
||||
'{available:true,time:$time,generation:"g",domains:[],devices:[],warnings:[],error:"",boot_protection:false} + $extra'
|
||||
}
|
||||
tb_wait() {
|
||||
tick=$((tick + 1))
|
||||
case $tick in
|
||||
1) expect 1 ;;
|
||||
2) expect 1; mkdir "$TB_SYSFS/domain0" ;; # idle: no reconcile
|
||||
3) expect 2; rmdir "$TB_SYSFS/domain0" ;; # a controller appeared
|
||||
4) expect 2; edit_state '.trusted.x={}' ;;
|
||||
5) expect 3; touch "${TB_STATE%/*}/boot-recovery.json" ;; # the policy changed
|
||||
6) expect 4; rm "${TB_STATE%/*}/boot-recovery.json" # a recovery checkpoint
|
||||
extra='{"devices":[{"Uid":"device-1"}]}'; edit_state '.trusted.y={}' ;;
|
||||
7) expect 5 ;; # devices from here on
|
||||
8) expect 6; rm "$TB_MARKER" ;; # so every tick reconciles
|
||||
esac
|
||||
}
|
||||
tb_daemon
|
||||
[[ $tick == 8 ]]
|
||||
}
|
||||
test_daemon_rechecks_after_the_idle_interval() {
|
||||
touch "$TB_MARKER"; TB_IDLE_RECHECK=2
|
||||
local tick=0
|
||||
tb_reconcile() {
|
||||
echo >> "$T/reconciles"
|
||||
jq -cn --argjson time "$EPOCHSECONDS" '{available:true,time:$time,generation:"g",domains:[],devices:[],warnings:[],error:"",boot_protection:false}'
|
||||
}
|
||||
tb_wait() {
|
||||
tick=$((tick + 1))
|
||||
case $tick in
|
||||
1) sleep 2.1 ;;
|
||||
2) [[ $(wc -l < "$T/reconciles") == 2 ]] || fail "the idle check did not expire"; rm "$TB_MARKER" ;;
|
||||
esac
|
||||
}
|
||||
tb_daemon
|
||||
}
|
||||
test_daemon_failed_reconcile_keeps_every_tick() {
|
||||
touch "$TB_MARKER"; TB_IDLE_RECHECK=600
|
||||
local tick=0
|
||||
tb_reconcile() { echo >> "$T/reconciles"; tb_fail 'Bolt unavailable'; }
|
||||
tb_wait() { tick=$((tick + 1)); (( tick < 3 )) || rm "$TB_MARKER"; }
|
||||
tb_daemon 2>/dev/null
|
||||
[[ $(wc -l < "$T/reconciles") == 3 ]]
|
||||
check '.available==false' "$TB_RUNTIME/snapshot.json"
|
||||
}
|
||||
test_watch_skips_only_unchanged_clean_snapshots() {
|
||||
local tick=0 start=$EPOCHSECONDS
|
||||
publish() { jq -cnS --argjson time "$1" --arg generation generation-a --argjson devices "${2:-[]}" \
|
||||
'{available:true,time:$time,generation:$generation,devices:$devices,warnings:[],error:""}' > "$TB_RUNTIME/snapshot.json"; }
|
||||
expect() { [[ $(wc -l < "$T/scans") == "$1" ]] || fail "tick $tick: expected $1 scans" "$(wc -l < "$T/scans")"; }
|
||||
tb_scan() { echo >> "$T/scans"; cat "$TB_RUNTIME/snapshot.json"; }
|
||||
publish "$start"
|
||||
tb_wait() {
|
||||
tick=$((tick + 1))
|
||||
case $tick in
|
||||
1) expect 1 ;;
|
||||
2) expect 1; touch "$TB_REQUESTS/request-x.json" ;; # unchanged: skipped
|
||||
3) expect 2; rm "$TB_REQUESTS/request-x.json" ;; # a request in flight
|
||||
4) expect 2; publish "$((start - 5))" ;;
|
||||
5) expect 3; echo generation-b > "$TB_RUNTIME/generation" ;; # a new snapshot
|
||||
6) expect 4; echo generation-a > "$TB_RUNTIME/generation" # another daemon
|
||||
publish "$start" '[{"identity":{}}]' ;;
|
||||
7) expect 5 ;; # devices from here on
|
||||
8) expect 6; exit 0 ;; # so every tick scans
|
||||
esac
|
||||
}
|
||||
(tb_watch)
|
||||
}
|
||||
test_watch_rescans_a_stale_snapshot() {
|
||||
local tick=0
|
||||
jq -cnS --argjson time "$((EPOCHSECONDS - 30))" \
|
||||
'{available:true,time:$time,generation:"generation-a",devices:[],warnings:[],error:""}' > "$TB_RUNTIME/snapshot.json"
|
||||
tb_scan() { echo >> "$T/scans"; cat "$TB_RUNTIME/snapshot.json"; }
|
||||
tb_wait() { tick=$((tick + 1)); (( tick < 3 )) || exit 0; }
|
||||
(tb_watch)
|
||||
[[ $(wc -l < "$T/scans") == 3 ]]
|
||||
}
|
||||
test_invalid_state_not_skipped_by_migration() {
|
||||
echo '{"enabled":false}' > "$TB_STATE"; reject tb_admin migrate
|
||||
[[ ! -s $T/units ]]
|
||||
|
||||
Reference in new issue
Block a user